896 lines
32 KiB
Go
896 lines
32 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const planetOccultationPathMaxTemporalSamples = 5000
|
||
|
|
|
||
|
|
type occultationPathFrameFunc func(float64) (occultationPathFrame, bool)
|
||
|
|
|
||
|
|
// FindPlanetOccultationPaths 搜索有限盘面行星月掩的全球外接触和内接触掩带。
|
||
|
|
// 查询窗口按全球几何掩甚点选择事件;端点容差 10 ms 与数值根精度一致。求解成功时,每条路径扩展到完整全球起止点。
|
||
|
|
// FindPlanetOccultationPaths searches the global outer- and inner-contact footprints of one finite-disk planet.
|
||
|
|
// 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 when solved.
|
||
|
|
func FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
|
||
|
|
options OccultationPathOptions) ([]PlanetOccultationPath, error) {
|
||
|
|
if err := validateOccultationTimeRange(start, end); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if err := planet.Validate(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if err := options.Validate(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
config, _ := planetOccultationConfigFor(planet)
|
||
|
|
options = normalizeOccultationPathOptions(options)
|
||
|
|
startTT := occultationTimeToTT(start)
|
||
|
|
endTT := occultationTimeToTT(end)
|
||
|
|
candidateStartTT := startTT - occultationPathSearchSpanDays
|
||
|
|
candidateEndTT := endTT + occultationPathSearchSpanDays
|
||
|
|
candidates := planetOccultationCandidateGreatestTimes(
|
||
|
|
candidateStartTT, candidateEndTT, planetOccultationDefaultStepDays, config, nil, 0,
|
||
|
|
)
|
||
|
|
paths := make([]PlanetOccultationPath, 0, len(candidates))
|
||
|
|
for _, seedTT := range candidates {
|
||
|
|
path, ok, err := planetOccultationPathAtSeed(seedTT, config, options, start.Location())
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !occultationTimeInSelectionWindow(path.Greatest.Time, start, end) {
|
||
|
|
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 planetOccultationPathAtSeed(
|
||
|
|
seedTT float64,
|
||
|
|
config planetOccultationConfig,
|
||
|
|
options OccultationPathOptions,
|
||
|
|
location *time.Location,
|
||
|
|
) (PlanetOccultationPath, bool, error) {
|
||
|
|
frameAt := func(tt float64) (occultationPathFrame, bool) {
|
||
|
|
return planetOccultationPathFrameAt(tt, config)
|
||
|
|
}
|
||
|
|
totalFrameAt := func(tt float64) (occultationPathFrame, bool) {
|
||
|
|
return planetOccultationTotalPathFrameAt(tt, config)
|
||
|
|
}
|
||
|
|
searchStart := seedTT - occultationPathSearchSpanDays
|
||
|
|
searchEnd := seedTT + occultationPathSearchSpanDays
|
||
|
|
outerStart, outerEnd, ok := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, false)
|
||
|
|
if !ok {
|
||
|
|
return PlanetOccultationPath{}, false, nil
|
||
|
|
}
|
||
|
|
centerStart, centerEnd, hasCenter := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, true)
|
||
|
|
greatestTT := occultationPathGreatestForFrame(seedTT, outerStart, outerEnd, frameAt)
|
||
|
|
greatest, greatestOK := occultationPathCenterPointForFrame(greatestTT, frameAt, location)
|
||
|
|
if !greatestOK && hasCenter {
|
||
|
|
greatestTT = math.Max(centerStart, math.Min(centerEnd, greatestTT))
|
||
|
|
greatest, greatestOK = occultationPathCenterPointForFrame(greatestTT, frameAt, location)
|
||
|
|
}
|
||
|
|
if !greatestOK {
|
||
|
|
greatest, greatestOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, location)
|
||
|
|
}
|
||
|
|
if !greatestOK {
|
||
|
|
return PlanetOccultationPath{}, false, nil
|
||
|
|
}
|
||
|
|
_, _, greatestWidth, greatestWidthOK := occultationPathLimitsAndWidthForFrame(greatestTT, frameAt)
|
||
|
|
if !greatestWidthOK || greatestWidth <= 0 {
|
||
|
|
return PlanetOccultationPath{}, false, nil
|
||
|
|
}
|
||
|
|
// 仅有边界的事件没有影轴与椭球交点;原回退点使用纬度极值弦宽,全掩带使用下方的地面横向宽度。统一两种接触带宽度定义,使有限盘面内外接触宽度可比较。
|
||
|
|
// Boundary-only events do not have an axis/ellipsoid intersection. Their fallback point used to carry a latitude-extrema chord width, while total bands used the ground cross-track width below. Keep both contact bands on the same width definition so finite-disk inner/outer widths are comparable.
|
||
|
|
greatest.WidthKM = greatestWidth
|
||
|
|
totalStartTT, totalEndTT, hasTotal := occultationPathWindowForFrame(
|
||
|
|
seedTT, searchStart, searchEnd, totalFrameAt, false,
|
||
|
|
)
|
||
|
|
hasTotal = hasTotal && greatestTT >= totalStartTT && greatestTT <= totalEndTT
|
||
|
|
if planetOccultationPathTemporalSampleCount(
|
||
|
|
outerStart, outerEnd, centerStart, centerEnd, hasCenter,
|
||
|
|
totalStartTT, totalEndTT, hasTotal, greatestTT, options,
|
||
|
|
) > planetOccultationPathMaxTemporalSamples {
|
||
|
|
return PlanetOccultationPath{}, false, ErrOccultationPathSamplingLimit
|
||
|
|
}
|
||
|
|
|
||
|
|
start := occultationPathBoundaryEndpointForFrame(outerStart, frameAt, location, 1)
|
||
|
|
end := occultationPathBoundaryEndpointForFrame(outerEnd, frameAt, location, -1)
|
||
|
|
if !start.valid || !end.valid {
|
||
|
|
return PlanetOccultationPath{}, false, nil
|
||
|
|
}
|
||
|
|
centerLine, northern, southern, err := planetOccultationPathSamples(
|
||
|
|
outerStart, outerEnd, centerStart, centerEnd, hasCenter, greatestTT, frameAt, options, location,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return PlanetOccultationPath{}, false, err
|
||
|
|
}
|
||
|
|
path := PlanetOccultationPath{
|
||
|
|
Planet: config.planet,
|
||
|
|
TargetID: config.planet.String(),
|
||
|
|
Start: start.point,
|
||
|
|
Greatest: greatest,
|
||
|
|
End: end.point,
|
||
|
|
Complete: outerStart > searchStart && outerEnd < searchEnd,
|
||
|
|
CenterLine: centerLine,
|
||
|
|
NorthernLimit: occultationPathWithEndpoints(start.point, end.point, northern),
|
||
|
|
SouthernLimit: occultationPathWithEndpoints(start.point, end.point, southern),
|
||
|
|
Step: options.Step,
|
||
|
|
TargetSpacingKM: options.TargetSpacingKM,
|
||
|
|
}
|
||
|
|
path.PartialFootprints = planetOccultationFootprints(
|
||
|
|
outerStart, outerEnd, greatestTT, frameAt, options, location,
|
||
|
|
)
|
||
|
|
|
||
|
|
if !hasTotal {
|
||
|
|
return path, true, nil
|
||
|
|
}
|
||
|
|
totalStart := occultationPathBoundaryEndpointForFrame(totalStartTT, totalFrameAt, location, 1)
|
||
|
|
totalEnd := occultationPathBoundaryEndpointForFrame(totalEndTT, totalFrameAt, location, -1)
|
||
|
|
_, _, totalWidth, totalWidthOK := occultationPathLimitsAndWidthForFrame(greatestTT, totalFrameAt)
|
||
|
|
if !totalStart.valid || !totalEnd.valid || !totalWidthOK || totalWidth <= 0 {
|
||
|
|
return path, true, nil
|
||
|
|
}
|
||
|
|
totalNorthern, totalSouthern := occultationPathBoundarySamplesForFrame(
|
||
|
|
totalStartTT, totalEndTT, greatestTT, totalFrameAt, options, location,
|
||
|
|
)
|
||
|
|
if len(totalNorthern) == 0 || len(totalSouthern) == 0 {
|
||
|
|
return path, true, nil
|
||
|
|
}
|
||
|
|
path.HasTotalBand = true
|
||
|
|
path.TotalStart = totalStart.point
|
||
|
|
path.TotalEnd = totalEnd.point
|
||
|
|
path.TotalComplete = totalStartTT > searchStart && totalEndTT < searchEnd
|
||
|
|
path.NorthernTotalLimit = occultationPathWithEndpoints(totalStart.point, totalEnd.point, totalNorthern)
|
||
|
|
path.SouthernTotalLimit = occultationPathWithEndpoints(totalStart.point, totalEnd.point, totalSouthern)
|
||
|
|
path.TotalFootprints = planetOccultationFootprints(
|
||
|
|
totalStartTT, totalEndTT, greatestTT, totalFrameAt, options, location,
|
||
|
|
)
|
||
|
|
path.GreatestTotalWidthKM = totalWidth
|
||
|
|
return path, true, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func planetOccultationPathTemporalSampleCount(
|
||
|
|
outerStartTT, outerEndTT float64,
|
||
|
|
centerStartTT, centerEndTT float64,
|
||
|
|
hasCenter bool,
|
||
|
|
totalStartTT, totalEndTT float64,
|
||
|
|
hasTotal bool,
|
||
|
|
greatestTT float64,
|
||
|
|
options OccultationPathOptions,
|
||
|
|
) int {
|
||
|
|
stepDays := float64(options.Step) / float64(24*time.Hour)
|
||
|
|
count := len(occultationPathSampleTimes(outerStartTT, outerEndTT, greatestTT, stepDays))
|
||
|
|
count += len(occultationPathSampleTimesWithLimit(
|
||
|
|
outerStartTT, outerEndTT, greatestTT, stepDays, planetOccultationFootprintMaxSamples,
|
||
|
|
))
|
||
|
|
if hasCenter {
|
||
|
|
count += len(occultationPathSampleTimes(centerStartTT, centerEndTT, greatestTT, stepDays))
|
||
|
|
}
|
||
|
|
if hasTotal {
|
||
|
|
count += len(occultationPathSampleTimes(totalStartTT, totalEndTT, greatestTT, stepDays))
|
||
|
|
count += len(occultationPathSampleTimesWithLimit(
|
||
|
|
totalStartTT, totalEndTT, greatestTT, stepDays, planetOccultationFootprintMaxSamples,
|
||
|
|
))
|
||
|
|
}
|
||
|
|
return count
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathWindowForFrame(
|
||
|
|
seedTT, startTT, endTT float64,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
center bool,
|
||
|
|
) (float64, float64, bool) {
|
||
|
|
left := math.Max(startTT, seedTT-occultationPathSearchSpanDays)
|
||
|
|
right := math.Min(endTT, seedTT+occultationPathSearchSpanDays)
|
||
|
|
if right <= left {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
predicate := func(tt float64) bool {
|
||
|
|
frame, ok := frameAt(tt)
|
||
|
|
if !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if center {
|
||
|
|
_, _, ok = occultationEarthLineIntersection(frame.moon, frame.axis)
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
return occultationPathFrameHasBoundary(frame)
|
||
|
|
}
|
||
|
|
|
||
|
|
step := occultationPathRangeStepDays
|
||
|
|
first := math.NaN()
|
||
|
|
previous := left
|
||
|
|
previousOK := predicate(previous)
|
||
|
|
if previousOK {
|
||
|
|
first = previous
|
||
|
|
} else {
|
||
|
|
for tt := left + step; tt <= right; tt += step {
|
||
|
|
current := math.Min(tt, right)
|
||
|
|
currentOK := predicate(current)
|
||
|
|
if currentOK {
|
||
|
|
first = occultationPathRefineTransition(previous, current, predicate, false)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
previous = current
|
||
|
|
previousOK = currentOK
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if math.IsNaN(first) {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
|
||
|
|
last := first
|
||
|
|
previous = first
|
||
|
|
previousOK = true
|
||
|
|
for tt := first + step; tt <= right; tt += step {
|
||
|
|
current := math.Min(tt, right)
|
||
|
|
currentOK := predicate(current)
|
||
|
|
if !currentOK {
|
||
|
|
last = occultationPathRefineTransition(previous, current, predicate, true)
|
||
|
|
return first, last, true
|
||
|
|
}
|
||
|
|
last = current
|
||
|
|
previous = current
|
||
|
|
previousOK = currentOK
|
||
|
|
}
|
||
|
|
if previousOK {
|
||
|
|
last = right
|
||
|
|
}
|
||
|
|
return first, last, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathGreatestForFrame(seedTT, startTT, endTT float64, frameAt occultationPathFrameFunc) float64 {
|
||
|
|
left := math.Max(startTT, seedTT-0.75)
|
||
|
|
right := math.Min(endTT, seedTT+0.75)
|
||
|
|
if right <= left {
|
||
|
|
return seedTT
|
||
|
|
}
|
||
|
|
impact := func(tt float64) float64 {
|
||
|
|
frame, ok := frameAt(tt)
|
||
|
|
if !ok {
|
||
|
|
return math.Inf(1)
|
||
|
|
}
|
||
|
|
return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY())
|
||
|
|
}
|
||
|
|
const goldenRatio = 0.6180339887498949
|
||
|
|
x1 := right - goldenRatio*(right-left)
|
||
|
|
x2 := left + goldenRatio*(right-left)
|
||
|
|
f1 := impact(x1)
|
||
|
|
f2 := impact(x2)
|
||
|
|
for i := 0; i < 56; i++ {
|
||
|
|
if f1 > f2 {
|
||
|
|
left = x1
|
||
|
|
x1, f1 = x2, f2
|
||
|
|
x2 = left + goldenRatio*(right-left)
|
||
|
|
f2 = impact(x2)
|
||
|
|
} else {
|
||
|
|
right = x2
|
||
|
|
x2, f2 = x1, f1
|
||
|
|
x1 = right - goldenRatio*(right-left)
|
||
|
|
f1 = impact(x1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return (left + right) / 2
|
||
|
|
}
|
||
|
|
|
||
|
|
func planetOccultationPathSamples(
|
||
|
|
outerStartTT, outerEndTT float64,
|
||
|
|
centerStartTT, centerEndTT float64,
|
||
|
|
hasCenter bool,
|
||
|
|
greatestTT float64,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
options OccultationPathOptions,
|
||
|
|
location *time.Location,
|
||
|
|
) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) {
|
||
|
|
var centerLine []OccultationPathPoint
|
||
|
|
if hasCenter {
|
||
|
|
var err error
|
||
|
|
centerLine, err = occultationPathCenterSamplesForFrame(centerStartTT, centerEndTT, greatestTT, frameAt, options, location)
|
||
|
|
if err != nil {
|
||
|
|
return nil, nil, nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
northern, southern := occultationPathBoundarySamplesForFrame(
|
||
|
|
outerStartTT, outerEndTT, greatestTT, frameAt, options, location,
|
||
|
|
)
|
||
|
|
return centerLine, northern, southern, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathBoundarySamplesForFrame(
|
||
|
|
startTT, endTT, greatestTT float64,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
options OccultationPathOptions,
|
||
|
|
location *time.Location,
|
||
|
|
) ([]OccultationPathPoint, []OccultationPathPoint) {
|
||
|
|
stepDays := float64(options.Step) / float64(24*time.Hour)
|
||
|
|
times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays)
|
||
|
|
samples := make([]occultationPathBoundaryPairSample, 0, len(times))
|
||
|
|
for _, tt := range times {
|
||
|
|
firstVector, secondVector, ok := occultationPathCrossTrackLimitsForFrame(tt, frameAt)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
sample := occultationPathBoundaryPairSample{tt: tt, first: firstVector, second: secondVector}
|
||
|
|
if len(samples) == 0 {
|
||
|
|
samples = append(samples, sample)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
samples = appendOccultationPathBoundaryPairSegment(samples, samples[len(samples)-1], sample, frameAt, 0)
|
||
|
|
}
|
||
|
|
first := make([]OccultationPathPoint, len(samples))
|
||
|
|
second := make([]OccultationPathPoint, len(samples))
|
||
|
|
for index, sample := range samples {
|
||
|
|
first[index] = occultationPathPointFromVector(sample.tt, sample.first, 0, location)
|
||
|
|
second[index] = occultationPathPointFromVector(sample.tt, sample.second, 0, location)
|
||
|
|
}
|
||
|
|
return occultationPathOrientBoundarySamples(first, second, greatestTT)
|
||
|
|
}
|
||
|
|
|
||
|
|
type occultationPathBoundaryPairSample struct {
|
||
|
|
tt float64
|
||
|
|
first, second occultationPathVector
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendOccultationPathBoundaryPairSegment(
|
||
|
|
samples []occultationPathBoundaryPairSample,
|
||
|
|
start, end occultationPathBoundaryPairSample,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
depth int,
|
||
|
|
) []occultationPathBoundaryPairSample {
|
||
|
|
if depth >= occultationPathMaxAdaptiveDepth ||
|
||
|
|
occultationPathBoundaryPairSpacing(start, end) <= occultationPathBoundarySpacingKM {
|
||
|
|
return append(samples, end)
|
||
|
|
}
|
||
|
|
midTT := (start.tt + end.tt) / 2
|
||
|
|
first, second, ok := occultationPathCrossTrackLimitsForFrame(midTT, frameAt)
|
||
|
|
if !ok {
|
||
|
|
return append(samples, end)
|
||
|
|
}
|
||
|
|
mid := occultationPathBoundaryPairSample{tt: midTT, first: first, second: second}
|
||
|
|
samples = appendOccultationPathBoundaryPairSegment(samples, start, mid, frameAt, depth+1)
|
||
|
|
return appendOccultationPathBoundaryPairSegment(samples, mid, end, frameAt, depth+1)
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathBoundaryPairSpacing(first, second occultationPathBoundaryPairSample) float64 {
|
||
|
|
firstA := occultationPathEarthFixedVector(first.tt, first.first)
|
||
|
|
firstB := occultationPathEarthFixedVector(first.tt, first.second)
|
||
|
|
secondA := occultationPathEarthFixedVector(second.tt, second.first)
|
||
|
|
secondB := occultationPathEarthFixedVector(second.tt, second.second)
|
||
|
|
direct := math.Max(
|
||
|
|
occultationPathNorm(occultationPathSub(secondA, firstA)),
|
||
|
|
occultationPathNorm(occultationPathSub(secondB, firstB)),
|
||
|
|
)
|
||
|
|
swapped := math.Max(
|
||
|
|
occultationPathNorm(occultationPathSub(secondB, firstA)),
|
||
|
|
occultationPathNorm(occultationPathSub(secondA, firstB)),
|
||
|
|
)
|
||
|
|
return math.Min(direct, swapped)
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathCrossTrackLimitsForFrame(
|
||
|
|
tt float64,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
) (occultationPathVector, occultationPathVector, bool) {
|
||
|
|
frame, ok := frameAt(tt)
|
||
|
|
before, beforeOK := frameAt(tt - occultationPathVelocityStepDays)
|
||
|
|
after, afterOK := frameAt(tt + occultationPathVelocityStepDays)
|
||
|
|
if !ok || !beforeOK || !afterOK {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
vx := after.moonProjectionX() - before.moonProjectionX()
|
||
|
|
vy := after.moonProjectionY() - before.moonProjectionY()
|
||
|
|
if math.Hypot(vx, vy) <= 1e-12 {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// 复用日食中心线构造:取基准面中垂直于运动方向的两条影锥母线。点源掠过阶段将缺失母线限制到可见角度区间;有限目标使用最近可见区间两端,直到两条横向母线分别与地球相交。
|
||
|
|
// Match the solar-eclipse central-path construction: take the two shadow generators perpendicular to motion in the fundamental plane. During a grazing point-source phase, clamp a missing generator to the visible-angle interval. For a finite target, use both ends of the nearest visible interval until the two cross-track generators intersect Earth independently.
|
||
|
|
theta := math.Atan2(vx, -vy)
|
||
|
|
first, _, firstOK := occultationPathBoundaryVector(frame, theta)
|
||
|
|
second, _, secondOK := occultationPathBoundaryVector(frame, theta+math.Pi)
|
||
|
|
if frame.targetRadius == 0 {
|
||
|
|
if !firstOK {
|
||
|
|
first, firstOK = occultationPathBoundaryAtNearestPointSourceTheta(frame, theta)
|
||
|
|
}
|
||
|
|
if !secondOK {
|
||
|
|
second, secondOK = occultationPathBoundaryAtNearestPointSourceTheta(frame, theta+math.Pi)
|
||
|
|
}
|
||
|
|
if !firstOK || !secondOK {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
return first, second, true
|
||
|
|
}
|
||
|
|
intervals := occultationPathBoundaryThetaIntervals(frame)
|
||
|
|
interval, intervalOK := occultationPathNearestThetaInterval(intervals, theta)
|
||
|
|
if firstOK && secondOK {
|
||
|
|
if intervalOK && occultationPathAngleDistance(interval.left, theta) > occultationPathAngleDistance(interval.left, theta+math.Pi) {
|
||
|
|
first, second = second, first
|
||
|
|
}
|
||
|
|
return first, second, true
|
||
|
|
}
|
||
|
|
if !intervalOK {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
first, _, firstOK = occultationPathBoundaryVector(frame, interval.left)
|
||
|
|
second, _, secondOK = occultationPathBoundaryVector(frame, interval.right)
|
||
|
|
if !firstOK || !secondOK {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
return first, second, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathAngleDistance(first, second float64) float64 {
|
||
|
|
return math.Abs(math.Remainder(first-second, 2*math.Pi))
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathBoundaryAtNearestPointSourceTheta(
|
||
|
|
frame occultationPathFrame,
|
||
|
|
theta float64,
|
||
|
|
) (occultationPathVector, bool) {
|
||
|
|
_, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame)
|
||
|
|
if !tangentOK {
|
||
|
|
return occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
left, right, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta)
|
||
|
|
if !intervalOK {
|
||
|
|
return occultationPathVector{}, false
|
||
|
|
}
|
||
|
|
middle := (left + right) / 2
|
||
|
|
theta += 2 * math.Pi * math.Round((middle-theta)/(2*math.Pi))
|
||
|
|
if theta < left {
|
||
|
|
theta = left
|
||
|
|
} else if theta > right {
|
||
|
|
theta = right
|
||
|
|
}
|
||
|
|
point, _, ok := occultationPathBoundaryVector(frame, theta)
|
||
|
|
return point, ok
|
||
|
|
}
|
||
|
|
|
||
|
|
type occultationPathThetaInterval struct {
|
||
|
|
left, right float64
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathNearestThetaInterval(
|
||
|
|
intervals []occultationPathThetaInterval,
|
||
|
|
theta float64,
|
||
|
|
) (occultationPathThetaInterval, bool) {
|
||
|
|
var closest occultationPathThetaInterval
|
||
|
|
closestDistance := math.Inf(1)
|
||
|
|
for _, interval := range intervals {
|
||
|
|
middle := (interval.left + interval.right) / 2
|
||
|
|
firstDelta := math.Abs(math.Remainder(theta-middle, 2*math.Pi))
|
||
|
|
secondDelta := math.Abs(math.Remainder(theta+math.Pi-middle, 2*math.Pi))
|
||
|
|
distance := math.Min(firstDelta, secondDelta)
|
||
|
|
if distance < closestDistance {
|
||
|
|
closest = interval
|
||
|
|
closestDistance = distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !finite(closestDistance) {
|
||
|
|
return occultationPathThetaInterval{}, false
|
||
|
|
}
|
||
|
|
return closest, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathBoundaryThetaIntervals(frame occultationPathFrame) []occultationPathThetaInterval {
|
||
|
|
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
|
||
|
|
discriminants := make([]float64, occultationPathBoundaryScanPoints)
|
||
|
|
for index := range discriminants {
|
||
|
|
discriminant, _, _, ok := occultationPathBoundaryLine(frame, step*float64(index))
|
||
|
|
if !ok {
|
||
|
|
discriminant = math.Inf(-1)
|
||
|
|
}
|
||
|
|
discriminants[index] = discriminant
|
||
|
|
}
|
||
|
|
|
||
|
|
intervals := make([]occultationPathThetaInterval, 0, 2)
|
||
|
|
for index, value := range discriminants {
|
||
|
|
previous := discriminants[(index+len(discriminants)-1)%len(discriminants)]
|
||
|
|
next := discriminants[(index+1)%len(discriminants)]
|
||
|
|
if value < previous || value < next {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
theta := occultationPathRefineBoundaryMaximum(frame, step*float64(index), step)
|
||
|
|
discriminant, b, scale, ok := occultationPathBoundaryLine(frame, theta)
|
||
|
|
tolerance := 1e-12 * math.Max(scale, 1)
|
||
|
|
if !ok || b >= 0 || discriminant < -tolerance {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
left, right, intervalOK := occultationPathBoundaryThetaInterval(frame, theta)
|
||
|
|
if intervalOK {
|
||
|
|
intervals = append(intervals, occultationPathThetaInterval{left: left, right: right})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return intervals
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathRefineBoundaryMaximum(frame occultationPathFrame, center, step float64) float64 {
|
||
|
|
left := center - step
|
||
|
|
right := center + step
|
||
|
|
const goldenRatio = 0.6180339887498949
|
||
|
|
x1 := right - goldenRatio*(right-left)
|
||
|
|
x2 := left + goldenRatio*(right-left)
|
||
|
|
f1, _, _, _ := occultationPathBoundaryLine(frame, x1)
|
||
|
|
f2, _, _, _ := occultationPathBoundaryLine(frame, x2)
|
||
|
|
for iteration := 0; iteration < 40; iteration++ {
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return (left + right) / 2
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathOrientBoundarySamples(
|
||
|
|
first, second []OccultationPathPoint,
|
||
|
|
greatestTT float64,
|
||
|
|
) ([]OccultationPathPoint, []OccultationPathPoint) {
|
||
|
|
if len(first) == 0 || len(first) != len(second) {
|
||
|
|
return first, second
|
||
|
|
}
|
||
|
|
nearest := 0
|
||
|
|
nearestDelta := math.Inf(1)
|
||
|
|
for index := range first {
|
||
|
|
delta := math.Abs(centerTimeTT(first[index].Time) - greatestTT)
|
||
|
|
if delta < nearestDelta {
|
||
|
|
nearest = index
|
||
|
|
nearestDelta = delta
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if first[nearest].Latitude >= second[nearest].Latitude {
|
||
|
|
return first, second
|
||
|
|
}
|
||
|
|
return second, first
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathCenterSamplesForFrame(
|
||
|
|
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 := occultationPathCenterPointForFrame(tt, frameAt, location)
|
||
|
|
if ok {
|
||
|
|
points = append(points, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if options.TargetSpacingKM > 0 {
|
||
|
|
return refineOccultationPathSpacingForFrame(points, frameAt, options.TargetSpacingKM, location)
|
||
|
|
}
|
||
|
|
return points, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func refineOccultationPathSpacingForFrame(
|
||
|
|
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 = appendOccultationPathSegmentForFrame(refined, points[i-1], points[i], frameAt, targetSpacingKM, location, 0)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
refineOccultationPathWidths(refined[segmentStart:], widthAt)
|
||
|
|
}
|
||
|
|
return refined, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendOccultationPathSegmentForFrame(
|
||
|
|
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
|
||
|
|
}
|
||
|
|
midTT := (centerTimeTT(start.Time) + centerTimeTT(end.Time)) / 2
|
||
|
|
mid, ok := occultationPathCenterPointForFrameWithoutWidth(midTT, frameAt, location)
|
||
|
|
if !ok {
|
||
|
|
return append(points, end), nil
|
||
|
|
}
|
||
|
|
mid.WidthKM = (start.WidthKM + end.WidthKM) / 2
|
||
|
|
var err error
|
||
|
|
points, err = appendOccultationPathSegmentForFrame(points, start, mid, frameAt, targetSpacingKM, location, depth+1)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return appendOccultationPathSegmentForFrame(points, mid, end, frameAt, targetSpacingKM, location, depth+1)
|
||
|
|
}
|
||
|
|
|
||
|
|
func planetOccultationPathFrameAt(tt float64, config planetOccultationConfig) (occultationPathFrame, bool) {
|
||
|
|
return planetOccultationContactPathFrameAt(tt, config, false)
|
||
|
|
}
|
||
|
|
|
||
|
|
func planetOccultationTotalPathFrameAt(tt float64, config planetOccultationConfig) (occultationPathFrame, bool) {
|
||
|
|
return planetOccultationContactPathFrameAt(tt, config, true)
|
||
|
|
}
|
||
|
|
|
||
|
|
func planetOccultationContactPathFrameAt(tt float64, config planetOccultationConfig, total bool) (occultationPathFrame, bool) {
|
||
|
|
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
|
||
|
|
moonDistance := HMoonAwayN(tt, -1)
|
||
|
|
planetRA, planetDec := config.apparentRaDecN(tt, -1)
|
||
|
|
planetDistance := config.earthDistanceN(tt, -1) * occultationPathAstronomicalUnitKM
|
||
|
|
if !finite(moonRA) || !finite(moonDec) || !finite(moonDistance) || moonDistance <= 0 ||
|
||
|
|
!finite(planetRA) || !finite(planetDec) || !finite(planetDistance) || planetDistance <= moonDistance {
|
||
|
|
return occultationPathFrame{}, false
|
||
|
|
}
|
||
|
|
moon := occultationPathRaDecVector(moonRA, moonDec, moonDistance)
|
||
|
|
target := occultationPathRaDecVector(planetRA, planetDec, planetDistance)
|
||
|
|
moonToTarget := occultationPathSub(target, moon)
|
||
|
|
moonToTargetDistance := occultationPathNorm(moonToTarget)
|
||
|
|
moonRadius := MoonSemidiameter(tt) * math.Pi / (180 * 3600)
|
||
|
|
moonRadiusKM := occultationPathNorm(moon) * math.Sin(moonRadius)
|
||
|
|
contactRadiusKM := moonRadiusKM + config.equatorialRadiusKM
|
||
|
|
if total {
|
||
|
|
contactRadiusKM = moonRadiusKM - config.equatorialRadiusKM
|
||
|
|
}
|
||
|
|
if moonToTargetDistance <= math.Abs(contactRadiusKM) {
|
||
|
|
return occultationPathFrame{}, false
|
||
|
|
}
|
||
|
|
axis := occultationPathUnit(occultationPathScale(moonToTarget, -1))
|
||
|
|
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: moonRadius,
|
||
|
|
targetRadius: math.Asin(contactRadiusKM / moonToTargetDistance),
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathFrameHasBoundary(frame occultationPathFrame) bool {
|
||
|
|
_, _, ok := occultationPathBoundaryTangent(frame)
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathBoundaryEndpointForFrame(
|
||
|
|
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*offset)*0.5/86400.0
|
||
|
|
frame, ok := frameAt(candidateTT)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
vector, _, valid := occultationPathBoundaryTangent(frame)
|
||
|
|
if valid {
|
||
|
|
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return occultationPathEndpoint{}
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathCenterPointForFrame(
|
||
|
|
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 occultationPathCenterPointForFrameWithoutWidth(
|
||
|
|
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 occultationPathBoundaryPointForFrame(
|
||
|
|
tt float64,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
location *time.Location,
|
||
|
|
) (OccultationPathPoint, bool) {
|
||
|
|
frame, ok := frameAt(tt)
|
||
|
|
if !ok {
|
||
|
|
return OccultationPathPoint{}, false
|
||
|
|
}
|
||
|
|
point, _, ok := occultationPathBoundaryTangent(frame)
|
||
|
|
if !ok {
|
||
|
|
return OccultationPathPoint{}, false
|
||
|
|
}
|
||
|
|
north, south, limitsOK := occultationPathScannedLimitsAtFrame(tt, frame)
|
||
|
|
return occultationPathPointFromVector(tt, point, occultationPathBoundaryWidth(north, south, limitsOK), location), true
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathLimitsAndWidthForFrame(
|
||
|
|
tt float64,
|
||
|
|
frameAt occultationPathFrameFunc,
|
||
|
|
) (occultationPathVector, occultationPathVector, float64, bool) {
|
||
|
|
frame, ok := frameAt(tt)
|
||
|
|
if !ok {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, 0, false
|
||
|
|
}
|
||
|
|
beforeFrame, beforeOK := frameAt(tt - occultationPathVelocityStepDays)
|
||
|
|
afterFrame, afterOK := frameAt(tt + occultationPathVelocityStepDays)
|
||
|
|
if !beforeOK || !afterOK {
|
||
|
|
north, south, scannedOK := occultationPathScannedLimitsForFrame(tt, frame)
|
||
|
|
return north, south, occultationPathBoundaryWidth(north, south, scannedOK), scannedOK
|
||
|
|
}
|
||
|
|
|
||
|
|
vx := afterFrame.moonProjectionX() - beforeFrame.moonProjectionX()
|
||
|
|
vy := afterFrame.moonProjectionY() - beforeFrame.moonProjectionY()
|
||
|
|
speed := math.Hypot(vx, vy)
|
||
|
|
if speed <= 1e-12 {
|
||
|
|
north, south, scannedOK := occultationPathScannedLimitsForFrame(tt, frame)
|
||
|
|
return north, south, occultationPathBoundaryWidth(north, south, scannedOK), scannedOK
|
||
|
|
}
|
||
|
|
planeCrossTrack := occultationPathUnit(occultationPathAdd(
|
||
|
|
occultationPathScale(frame.first, -vy/speed),
|
||
|
|
occultationPathScale(frame.second, vx/speed),
|
||
|
|
))
|
||
|
|
|
||
|
|
var centerFixed, groundCrossTrack occultationPathVector
|
||
|
|
groundWidthOK := false
|
||
|
|
center, centerOK := occultationPathTrackReference(frame)
|
||
|
|
before, beforeCenterOK := occultationPathTrackReference(beforeFrame)
|
||
|
|
after, afterCenterOK := occultationPathTrackReference(afterFrame)
|
||
|
|
if centerOK && beforeCenterOK && afterCenterOK {
|
||
|
|
centerFixed = occultationPathEarthFixedVector(tt, center)
|
||
|
|
beforeFixed := occultationPathEarthFixedVector(tt-occultationPathVelocityStepDays, before)
|
||
|
|
afterFixed := occultationPathEarthFixedVector(tt+occultationPathVelocityStepDays, after)
|
||
|
|
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
|
||
|
|
normal := occultationPathUnit(occultationPathVector{x: centerFixed.x, y: centerFixed.y, z: centerFixed.z / polarRatioSquared})
|
||
|
|
track := occultationPathSub(afterFixed, beforeFixed)
|
||
|
|
track = occultationPathSub(track, occultationPathScale(normal, occultationPathDot(track, normal)))
|
||
|
|
if occultationPathNorm(track) > 1e-12 {
|
||
|
|
groundCrossTrack = occultationPathUnit(occultationPathCross(normal, occultationPathUnit(track)))
|
||
|
|
groundWidthOK = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
minimumOffset := math.Inf(1)
|
||
|
|
maximumOffset := math.Inf(-1)
|
||
|
|
minimumGroundOffset := math.Inf(1)
|
||
|
|
maximumGroundOffset := math.Inf(-1)
|
||
|
|
var minimumPoint, maximumPoint occultationPathVector
|
||
|
|
var minimumGroundPoint, maximumGroundPoint occultationPathVector
|
||
|
|
consider := func(point occultationPathVector) {
|
||
|
|
offset := occultationPathDot(point, planeCrossTrack)
|
||
|
|
if offset < minimumOffset {
|
||
|
|
minimumOffset = offset
|
||
|
|
minimumPoint = point
|
||
|
|
}
|
||
|
|
if offset > maximumOffset {
|
||
|
|
maximumOffset = offset
|
||
|
|
maximumPoint = point
|
||
|
|
}
|
||
|
|
if groundWidthOK {
|
||
|
|
fixed := occultationPathEarthFixedVector(tt, point)
|
||
|
|
groundOffset := occultationPathDot(occultationPathSub(fixed, centerFixed), groundCrossTrack)
|
||
|
|
if groundOffset < minimumGroundOffset {
|
||
|
|
minimumGroundOffset = groundOffset
|
||
|
|
minimumGroundPoint = point
|
||
|
|
}
|
||
|
|
if groundOffset > maximumGroundOffset {
|
||
|
|
maximumGroundOffset = groundOffset
|
||
|
|
maximumGroundPoint = point
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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 point, _, pointOK := occultationPathBoundaryVector(frame, theta); pointOK {
|
||
|
|
consider(point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for i := 0; i < occultationPathBoundaryScanPoints; i++ {
|
||
|
|
point, _, pointOK := occultationPathBoundaryVector(frame, 2*math.Pi*float64(i)/float64(occultationPathBoundaryScanPoints))
|
||
|
|
if !pointOK {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
consider(point)
|
||
|
|
}
|
||
|
|
if !finite(minimumOffset) || !finite(maximumOffset) {
|
||
|
|
return occultationPathVector{}, occultationPathVector{}, 0, false
|
||
|
|
}
|
||
|
|
width := maximumOffset - minimumOffset
|
||
|
|
if groundWidthOK && finite(minimumGroundOffset) && finite(maximumGroundOffset) {
|
||
|
|
width = maximumGroundOffset - minimumGroundOffset
|
||
|
|
// 有限目标会把月影打开或收束成圆锥;接近地平线时其影面投影可能折叠,使全球投影极值在远处地平线交点间跳变。地面轨迹极值仍位于掩带的同一物理侧。
|
||
|
|
// A finite target opens or closes the lunar shadow into a cone. Near the horizon its shadow-plane projection can fold, causing the global projected extremum to jump between distant horizon intersections. Ground-track extrema remain on the same physical sides of the band.
|
||
|
|
if frame.targetRadius != 0 {
|
||
|
|
return maximumGroundPoint, minimumGroundPoint, width, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_, minimumLatitude := occultationPathGeodetic(tt, minimumPoint)
|
||
|
|
_, maximumLatitude := occultationPathGeodetic(tt, maximumPoint)
|
||
|
|
if maximumLatitude >= minimumLatitude {
|
||
|
|
return maximumPoint, minimumPoint, width, true
|
||
|
|
}
|
||
|
|
return minimumPoint, maximumPoint, width, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathScannedLimitsForFrame(
|
||
|
|
tt float64,
|
||
|
|
frame occultationPathFrame,
|
||
|
|
) (occultationPathVector, occultationPathVector, bool) {
|
||
|
|
return occultationPathScannedLimitsAtFrame(tt, frame)
|
||
|
|
}
|