feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,895 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user