Files
astro/basic/occultation_planet_path.go
T

896 lines
32 KiB
Go
Raw Normal View History

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)
}