2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
1242 lines
48 KiB
Go
1242 lines
48 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
func appendOccultationRiseSetSamples(
|
|
tracks []*occultationRiseSetTrack,
|
|
points []OccultationPathPoint,
|
|
stepDays float64,
|
|
) []*occultationRiseSetTrack {
|
|
sort.SliceStable(points, func(first, second int) bool {
|
|
return points[first].Longitude < points[second].Longitude
|
|
})
|
|
type match struct {
|
|
point, track int
|
|
distance float64
|
|
}
|
|
var matches []match
|
|
for pointIndex, point := range points {
|
|
for index, track := range tracks {
|
|
if len(track.segments) == 0 || len(track.segments[len(track.segments)-1]) == 0 {
|
|
continue
|
|
}
|
|
last := track.segments[len(track.segments)-1][len(track.segments[len(track.segments)-1])-1]
|
|
deltaDays := point.Time.Sub(last.Time).Hours() / 24
|
|
if deltaDays <= 0 || deltaDays > 2.5*stepDays {
|
|
continue
|
|
}
|
|
distance := occultationPathDistanceKM(last, point)
|
|
if riseSetGeographicBranchChanged(distance, deltaDays) {
|
|
continue
|
|
}
|
|
matches = append(matches, match{point: pointIndex, track: index, distance: distance})
|
|
}
|
|
}
|
|
// Match the closest pairs before creating tracks for newly appearing
|
|
// roots. Longitude order must not let a new root claim an existing branch
|
|
// ahead of that branch's much closer continuation.
|
|
sort.SliceStable(matches, func(i, j int) bool { return matches[i].distance < matches[j].distance })
|
|
usedTracks, usedPoints := make([]bool, len(tracks)), make([]bool, len(points))
|
|
for _, candidate := range matches {
|
|
if usedTracks[candidate.track] || usedPoints[candidate.point] {
|
|
continue
|
|
}
|
|
usedTracks[candidate.track], usedPoints[candidate.point] = true, true
|
|
track := tracks[candidate.track]
|
|
track.segments[len(track.segments)-1] = append(track.segments[len(track.segments)-1], points[candidate.point])
|
|
}
|
|
for index, point := range points {
|
|
if !usedPoints[index] {
|
|
tracks = append(tracks, &occultationRiseSetTrack{segments: [][]OccultationPathPoint{{point}}})
|
|
}
|
|
}
|
|
return tracks
|
|
}
|
|
|
|
func completeOccultationRiseSetCurveEndpoints(
|
|
curves []OccultationRiseSetCurve,
|
|
stepDays float64,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) {
|
|
for index := range curves {
|
|
completeOccultationRiseSetFoldEndpoints(&curves[index], stepDays, location, cache)
|
|
}
|
|
completeOccultationRiseSetPhaseJunctions(curves, stepDays, location, cache)
|
|
completeOccultationRiseSetDirectionJunctions(curves, stepDays, location, cache)
|
|
refinementCache := cache.candidateOnly()
|
|
for index := range curves {
|
|
// Split sampled branch jumps before inserting adaptive midpoints. If a
|
|
// midpoint root is singular, refinement must not turn that jump back into
|
|
// a long straight segment.
|
|
normalizeOccultationRiseSetCurveSegments(&curves[index])
|
|
refineOccultationRiseSetCurveSpacing(&curves[index], location, refinementCache)
|
|
normalizeOccultationRiseSetCurveSegments(&curves[index])
|
|
}
|
|
refineOccultationRiseSetPhaseJunctionApproaches(curves, location, refinementCache)
|
|
for index := range curves {
|
|
normalizeOccultationRiseSetCurveSegments(&curves[index])
|
|
}
|
|
}
|
|
|
|
// normalizeOccultationRiseSetCurveSegments keeps each rendered branch
|
|
// strictly time-ordered. A completed horizon fold is a branch boundary when
|
|
// its time reverses; roots at the same location and time are numerical dupes.
|
|
func normalizeOccultationRiseSetCurveSegments(curve *OccultationRiseSetCurve) {
|
|
if curve == nil {
|
|
return
|
|
}
|
|
segments := make([][]OccultationPathPoint, 0, len(curve.Segments))
|
|
for _, segment := range curve.Segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
current := []OccultationPathPoint{segment[0]}
|
|
for _, point := range segment[1:] {
|
|
last := current[len(current)-1]
|
|
pointTT, lastTT := occultationTimeToTT(point.Time), occultationTimeToTT(last.Time)
|
|
if pointTT > lastTT+occultationRiseSetTimeEpsilonDays {
|
|
if occultationRiseSetBranchChanged(
|
|
occultationPathDistanceKM(last, point), pointTT-lastTT,
|
|
) {
|
|
if len(current) >= 2 {
|
|
segments = append(segments, current)
|
|
}
|
|
current = []OccultationPathPoint{point}
|
|
continue
|
|
}
|
|
current = append(current, point)
|
|
continue
|
|
}
|
|
if math.Abs(pointTT-lastTT) <= occultationRiseSetTimeEpsilonDays &&
|
|
occultationPathDistanceKM(point, last) <= 0.01 {
|
|
continue
|
|
}
|
|
if len(current) >= 2 {
|
|
segments = append(segments, current)
|
|
}
|
|
current = []OccultationPathPoint{point}
|
|
}
|
|
if len(current) >= 2 {
|
|
segments = append(segments, current)
|
|
}
|
|
}
|
|
curve.Segments = segments
|
|
}
|
|
|
|
type occultationRiseSetEndpoint struct {
|
|
curveIndex int
|
|
segmentIndex int
|
|
atStart bool
|
|
point OccultationPathPoint
|
|
}
|
|
|
|
type occultationRiseSetPhaseAttachment struct {
|
|
endpoint occultationRiseSetEndpoint
|
|
fold OccultationPathPoint
|
|
hasFold bool
|
|
}
|
|
|
|
func completeOccultationRiseSetFoldEndpoints(
|
|
curve *OccultationRiseSetCurve,
|
|
stepDays float64,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) {
|
|
if curve == nil || len(curve.Segments) < 2 {
|
|
return
|
|
}
|
|
endpoints := make([]occultationRiseSetEndpoint, 0, 2*len(curve.Segments))
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
endpoints = append(endpoints, occultationRiseSetEndpoint{
|
|
segmentIndex: segmentIndex, atStart: atStart,
|
|
point: occultationRiseSetSegmentEndpoint(segment, atStart),
|
|
})
|
|
}
|
|
}
|
|
used := make(map[[2]int]bool, len(endpoints))
|
|
for firstIndex := 0; firstIndex < len(endpoints); firstIndex++ {
|
|
first := endpoints[firstIndex]
|
|
firstKey := [2]int{first.segmentIndex, boolInt(first.atStart)}
|
|
if used[firstKey] {
|
|
continue
|
|
}
|
|
for secondIndex := firstIndex + 1; secondIndex < len(endpoints); secondIndex++ {
|
|
second := endpoints[secondIndex]
|
|
secondKey := [2]int{second.segmentIndex, boolInt(second.atStart)}
|
|
if first.segmentIndex == second.segmentIndex || first.atStart != second.atStart || used[secondKey] {
|
|
continue
|
|
}
|
|
// Close roots can cease to be resolved on adjacent time samples.
|
|
// Candidate pairing may span one sample; the fold solver and its
|
|
// physical residuals still decide whether the branches really meet.
|
|
if math.Abs(occultationTimeToTT(first.point.Time)-occultationTimeToTT(second.point.Time)) > math.Max(1e-8, 1.5*stepDays) {
|
|
continue
|
|
}
|
|
distance := occultationPathDistanceKM(first.point, second.point)
|
|
if distance <= 0.01 || distance > 3000 {
|
|
continue
|
|
}
|
|
if !occultationRiseSetFoldCandidate(
|
|
curve.Segments[first.segmentIndex], first.atStart,
|
|
curve.Segments[second.segmentIndex], second.atStart,
|
|
) {
|
|
continue
|
|
}
|
|
fold, ok := refineOccultationRiseSetFold(
|
|
first.point, second.point, first.atStart,
|
|
curve.Phase == RiseSetPhaseGreatest, stepDays, location, cache,
|
|
)
|
|
if !ok || math.Abs(occultationTimeToTT(fold.Time)-(occultationTimeToTT(first.point.Time)+occultationTimeToTT(second.point.Time))/2) > 2.5*stepDays ||
|
|
occultationPathDistanceKM(fold, first.point) > 3000 || occultationPathDistanceKM(fold, second.point) > 3000 {
|
|
continue
|
|
}
|
|
curve.Segments[first.segmentIndex] = occultationRiseSetAddEndpoint(curve.Segments[first.segmentIndex], fold, first.atStart)
|
|
curve.Segments[second.segmentIndex] = occultationRiseSetAddEndpoint(curve.Segments[second.segmentIndex], fold, second.atStart)
|
|
used[firstKey], used[secondKey] = true, true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// occultationRiseSetFoldCandidate reports whether two nearby endpoints are
|
|
// actually separating into different sampled branches. Endpoints that merely
|
|
// happen to be close are common on smooth curves and do not need a fold root.
|
|
func occultationRiseSetFoldCandidate(
|
|
first []OccultationPathPoint,
|
|
firstAtStart bool,
|
|
second []OccultationPathPoint,
|
|
secondAtStart bool,
|
|
) bool {
|
|
firstAdjacent, firstOK := occultationRiseSetAdjacentPoint(first, firstAtStart)
|
|
secondAdjacent, secondOK := occultationRiseSetAdjacentPoint(second, secondAtStart)
|
|
if !firstOK || !secondOK {
|
|
return true
|
|
}
|
|
endpointDistance := occultationPathDistanceKM(
|
|
occultationRiseSetSegmentEndpoint(first, firstAtStart),
|
|
occultationRiseSetSegmentEndpoint(second, secondAtStart),
|
|
)
|
|
adjacentDistance := occultationPathDistanceKM(firstAdjacent, secondAdjacent)
|
|
margin := 250.0
|
|
firstEndpoint := occultationRiseSetSegmentEndpoint(first, firstAtStart)
|
|
secondEndpoint := occultationRiseSetSegmentEndpoint(second, secondAtStart)
|
|
if math.Max(math.Abs(firstEndpoint.Latitude), math.Abs(secondEndpoint.Latitude)) >= 70 {
|
|
// Longitude convergence makes the first post-fold samples unusually
|
|
// close near a pole. Leave numerical room for a 10-second sample to
|
|
// land just inside the ordinary 250 km divergence margin.
|
|
margin = 200
|
|
}
|
|
return adjacentDistance > endpointDistance+margin
|
|
}
|
|
|
|
func occultationRiseSetAdjacentPoint(segment []OccultationPathPoint, atStart bool) (OccultationPathPoint, bool) {
|
|
if len(segment) < 2 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
if atStart {
|
|
return segment[1], true
|
|
}
|
|
return segment[len(segment)-2], true
|
|
}
|
|
|
|
func completeOccultationRiseSetPhaseJunctions(
|
|
curves []OccultationRiseSetCurve,
|
|
stepDays float64,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) {
|
|
curveIndices := make(map[occultationRiseSetCurveKey]int, len(curves))
|
|
for index, curve := range curves {
|
|
curveIndices[occultationRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
|
|
}
|
|
for _, direction := range []RiseSetDirection{RiseSetDirectionRise, RiseSetDirectionSet} {
|
|
startIndex, haveStart := curveIndices[occultationRiseSetCurveKey{phase: RiseSetPhaseStart, direction: direction}]
|
|
greatestIndex, haveGreatest := curveIndices[occultationRiseSetCurveKey{phase: RiseSetPhaseGreatest, direction: direction}]
|
|
endIndex, haveEnd := curveIndices[occultationRiseSetCurveKey{phase: RiseSetPhaseEnd, direction: direction}]
|
|
if !haveStart || !haveGreatest || !haveEnd {
|
|
continue
|
|
}
|
|
seeds := occultationRiseSetUnsharedEndpoints(startIndex, curves[startIndex].Segments)
|
|
for _, seed := range seeds {
|
|
candidateSeeds := []OccultationPathPoint{seed.point}
|
|
if endEndpoint, endOK := occultationRiseSetClosestEndpoint(seed.point, curves[endIndex].Segments, stepDays); endOK {
|
|
candidateSeeds = append([]OccultationPathPoint{
|
|
occultationRiseSetMidpoint(seed.point, endEndpoint.point),
|
|
endEndpoint.point,
|
|
}, candidateSeeds...)
|
|
}
|
|
junction, ok := OccultationPathPoint{}, false
|
|
for _, candidate := range candidateSeeds {
|
|
junction, ok = refineOccultationRiseSetPhaseJunctionOnHorizon(candidate, location, cache)
|
|
if !ok {
|
|
junction, ok = refineOccultationRiseSetPhaseJunction(candidate, location, cache)
|
|
}
|
|
if ok {
|
|
break
|
|
}
|
|
}
|
|
if !ok || math.Abs(occultationTimeToTT(junction.Time)-occultationTimeToTT(seed.point.Time)) > 3*stepDays ||
|
|
occultationPathDistanceKM(junction, seed.point) > 3000 {
|
|
continue
|
|
}
|
|
matched := make([]occultationRiseSetPhaseAttachment, 0, 3)
|
|
attachmentTargets := make(map[int]OccultationPathPoint, 1)
|
|
recovery := occultationRiseSetNarrowPhaseRecovery{}
|
|
recovered := false
|
|
// Contact extrema include the changing apparent radii; greatest
|
|
// separation need not end at exactly the same time. Complete the
|
|
// start/end pair even when the diagnostic greatest branch differs.
|
|
for _, curveIndex := range []int{startIndex, endIndex, greatestIndex} {
|
|
endpoint, endpointOK := occultationRiseSetClosestEndpoint(
|
|
junction, curves[curveIndex].Segments, stepDays,
|
|
)
|
|
if !endpointOK {
|
|
if curveIndex == greatestIndex {
|
|
continue
|
|
}
|
|
matched = nil
|
|
break
|
|
}
|
|
endpoint.curveIndex = curveIndex
|
|
attachment := occultationRiseSetPhaseAttachment{endpoint: endpoint}
|
|
if curveIndex == greatestIndex && occultationPathDistanceKM(junction, endpoint.point) > occultationPathBoundarySpacingKM {
|
|
if !occultationRiseSetPhaseSegmentIsContinuous(
|
|
junction, endpoint.point, RiseSetPhaseGreatest, direction,
|
|
location, cache,
|
|
) {
|
|
fold, foldOK := refineOccultationRiseSetFold(
|
|
junction, endpoint.point, endpoint.atStart, true,
|
|
stepDays, location, cache,
|
|
)
|
|
if !foldOK || !occultationRiseSetFoldBridgesPhaseJunction(
|
|
junction, endpoint, fold, direction, cache,
|
|
) {
|
|
recovery, recovered = occultationRiseSetRecoverNarrowPhaseJunction(
|
|
junction, endpoint, direction, stepDays, location, cache,
|
|
)
|
|
if !recovered {
|
|
break
|
|
}
|
|
attachmentTargets[curveIndex] = recovery.junction
|
|
} else {
|
|
attachment.fold, attachment.hasFold = fold, true
|
|
}
|
|
}
|
|
}
|
|
matched = append(matched, attachment)
|
|
}
|
|
if len(matched) < 2 {
|
|
continue
|
|
}
|
|
for _, attachment := range matched {
|
|
endpoint := attachment.endpoint
|
|
target := junction
|
|
if recoveredTarget, ok := attachmentTargets[endpoint.curveIndex]; ok {
|
|
target = recoveredTarget
|
|
}
|
|
if attachment.hasFold {
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex], attachment.fold, endpoint.atStart,
|
|
)
|
|
shortBranch := []OccultationPathPoint{attachment.fold, target}
|
|
if !endpoint.atStart {
|
|
shortBranch[0], shortBranch[1] = shortBranch[1], shortBranch[0]
|
|
}
|
|
curves[endpoint.curveIndex].Segments = append(curves[endpoint.curveIndex].Segments, shortBranch)
|
|
continue
|
|
}
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex], target, endpoint.atStart,
|
|
)
|
|
}
|
|
if recovered {
|
|
if segment := recovery.segments[RiseSetPhaseStart]; len(segment) >= 2 {
|
|
curves[startIndex].Segments = append(curves[startIndex].Segments, segment)
|
|
}
|
|
if segment := recovery.segments[RiseSetPhaseEnd]; len(segment) >= 2 {
|
|
curves[endIndex].Segments = append(curves[endIndex].Segments, segment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func occultationRiseSetPhaseSegmentIsContinuous(
|
|
start, end OccultationPathPoint,
|
|
phase RiseSetPhase,
|
|
direction RiseSetDirection,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) bool {
|
|
startTT := occultationTimeToTT(start.Time)
|
|
endTT := occultationTimeToTT(end.Time)
|
|
if math.Abs(endTT-startTT)*86400 < 0.1 {
|
|
return false
|
|
}
|
|
totalDistance := occultationPathDistanceKM(start, end)
|
|
continuityToleranceKM := math.Max(100, 0.1*totalDistance)
|
|
candidate := end
|
|
for divisor := 2.0; divisor <= 1024; divisor *= 2 {
|
|
tt := startTT + (endTT-startTT)/divisor
|
|
seedAngle := occultationRiseSetHorizonAngle(
|
|
tt, candidate.Longitude, candidate.Latitude, cache.context,
|
|
)
|
|
next, ok := occultationRiseSetPhasePointOnHorizon(
|
|
tt, seedAngle, phase, direction, location, cache,
|
|
)
|
|
if !ok {
|
|
return occultationPathDistanceKM(start, candidate) <= continuityToleranceKM
|
|
}
|
|
candidate = next
|
|
// At a three-phase junction the root becomes numerically singular.
|
|
// Once the traced branch is already inside the spatial tolerance,
|
|
// continuing toward the exact junction can jump to its sibling root.
|
|
if occultationPathDistanceKM(start, candidate) <= continuityToleranceKM {
|
|
return true
|
|
}
|
|
}
|
|
return occultationPathDistanceKM(start, candidate) <= continuityToleranceKM
|
|
}
|
|
|
|
type occultationRiseSetNarrowPhaseRecovery struct {
|
|
junction OccultationPathPoint
|
|
segments map[RiseSetPhase][]OccultationPathPoint
|
|
}
|
|
|
|
// A pair of phase junctions can be only a few seconds apart. The ordinary
|
|
// horizon scan cannot resolve both roots, so continue the already sampled
|
|
// greatest-phase branch to its contact crossing and reconstruct the two short
|
|
// contact branches only for that local degeneracy.
|
|
func occultationRiseSetRecoverNarrowPhaseJunction(
|
|
primary OccultationPathPoint,
|
|
greatestEndpoint occultationRiseSetEndpoint,
|
|
direction RiseSetDirection,
|
|
stepDays float64,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (occultationRiseSetNarrowPhaseRecovery, bool) {
|
|
fold, ok := refineOccultationRiseSetFold(
|
|
primary, greatestEndpoint.point, greatestEndpoint.atStart,
|
|
true, stepDays, location, cache,
|
|
)
|
|
if !ok {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
primaryTT := occultationTimeToTT(primary.Time)
|
|
endpointTT := occultationTimeToTT(greatestEndpoint.point.Time)
|
|
foldTT := occultationTimeToTT(fold.Time)
|
|
extensionDirection := primaryTT - endpointTT
|
|
if extensionDirection == 0 || (foldTT-primaryTT)*extensionDirection <= 0 {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
seedAngle := occultationRiseSetHorizonAngle(
|
|
primaryTT, greatestEndpoint.point.Longitude, greatestEndpoint.point.Latitude, cache.context,
|
|
)
|
|
branchPoint, branchEvaluation, ok := occultationRiseSetRawPhasePointOnHorizon(
|
|
primaryTT, seedAngle, true, location, cache,
|
|
)
|
|
if !ok || !occultationRiseSetDirectionMatches(
|
|
branchEvaluation, branchPoint, direction,
|
|
) {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
branchContact := branchEvaluation.center.stateAt(
|
|
branchPoint.Longitude, branchPoint.Latitude,
|
|
).contactMetric
|
|
foldEvaluation := cache.evaluation(foldTT)
|
|
foldContact := foldEvaluation.center.stateAt(fold.Longitude, fold.Latitude).contactMetric
|
|
if !finite(branchContact) || !finite(foldContact) || branchContact >= 0 || foldContact <= 0 {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
|
|
inside, outside := branchPoint, fold
|
|
insideContact, outsideContact := branchContact, foldContact
|
|
for iteration := 0; iteration < 56; iteration++ {
|
|
insideTT := occultationTimeToTT(inside.Time)
|
|
outsideTT := occultationTimeToTT(outside.Time)
|
|
if math.Abs(outsideTT-insideTT) <= 1e-10 {
|
|
break
|
|
}
|
|
middleTT := (insideTT + outsideTT) / 2
|
|
insideAngle := occultationRiseSetHorizonAngle(
|
|
middleTT, inside.Longitude, inside.Latitude, cache.context,
|
|
)
|
|
outsideAngle := occultationRiseSetHorizonAngle(
|
|
middleTT, outside.Longitude, outside.Latitude, cache.context,
|
|
)
|
|
if !finite(insideAngle) || !finite(outsideAngle) {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
middleAngle := riseSetNormalizeRadians(
|
|
insideAngle + math.Remainder(outsideAngle-insideAngle, 2*math.Pi)/2,
|
|
)
|
|
middle, evaluation, middleOK := occultationRiseSetRawPhasePointOnHorizon(
|
|
middleTT, middleAngle, true, location, cache,
|
|
)
|
|
if !middleOK || !occultationRiseSetDirectionMatches(evaluation, middle, direction) {
|
|
outside = OccultationPathPoint{
|
|
Time: occultationTTToLocation(middleTT, location),
|
|
Longitude: normalizeLongitude(
|
|
inside.Longitude + math.Remainder(outside.Longitude-inside.Longitude, 360)/2,
|
|
),
|
|
Latitude: (inside.Latitude + outside.Latitude) / 2,
|
|
}
|
|
continue
|
|
}
|
|
contact := evaluation.center.stateAt(middle.Longitude, middle.Latitude).contactMetric
|
|
if !finite(contact) {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
if contact <= 0 {
|
|
inside, insideContact = middle, contact
|
|
} else {
|
|
outside, outsideContact = middle, contact
|
|
}
|
|
}
|
|
secondary := inside
|
|
if math.Abs(outsideContact) < math.Abs(insideContact) {
|
|
secondary = outside
|
|
}
|
|
secondaryTT := occultationTimeToTT(secondary.Time)
|
|
secondaryEvaluation := cache.evaluation(secondaryTT)
|
|
secondaryState := secondaryEvaluation.center.stateAt(secondary.Longitude, secondary.Latitude)
|
|
if !secondaryState.valid || math.Abs(secondaryState.contactMetric) > 1e-7 ||
|
|
math.Abs(secondaryEvaluation.separationDerivative(secondary.Longitude, secondary.Latitude)) > 1e-7 ||
|
|
math.Abs(secondaryState.moonAltitude) > 1e-7 ||
|
|
!occultationRiseSetDirectionMatches(secondaryEvaluation, secondary, direction) ||
|
|
math.Abs(secondaryTT-primaryTT) > stepDays ||
|
|
occultationPathDistanceKM(primary, secondary) > 500 {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
|
|
phaseRoots, ok := occultationRiseSetNarrowContactRoots(
|
|
primary, secondary, direction, location, cache,
|
|
)
|
|
if !ok {
|
|
return occultationRiseSetNarrowPhaseRecovery{}, false
|
|
}
|
|
first, last := primary, secondary
|
|
if last.Time.Before(first.Time) {
|
|
first, last = last, first
|
|
}
|
|
segments := make(map[RiseSetPhase][]OccultationPathPoint, 2)
|
|
for phase, root := range phaseRoots {
|
|
segments[phase] = []OccultationPathPoint{first, root, last}
|
|
}
|
|
return occultationRiseSetNarrowPhaseRecovery{junction: secondary, segments: segments}, true
|
|
}
|
|
|
|
func occultationRiseSetDirectionMatches(
|
|
evaluation occultationRiseSetEvaluation,
|
|
point OccultationPathPoint,
|
|
direction RiseSetDirection,
|
|
) bool {
|
|
derivative := evaluation.moonAltitudeDerivative(point.Longitude, point.Latitude)
|
|
return finite(derivative) && ((direction == RiseSetDirectionRise && derivative > 0) ||
|
|
(direction == RiseSetDirectionSet && derivative < 0))
|
|
}
|
|
|
|
func occultationRiseSetNarrowContactRoots(
|
|
first, second OccultationPathPoint,
|
|
direction RiseSetDirection,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (map[RiseSetPhase]OccultationPathPoint, bool) {
|
|
tt := (occultationTimeToTT(first.Time) + occultationTimeToTT(second.Time)) / 2
|
|
firstAngle := occultationRiseSetHorizonAngle(tt, first.Longitude, first.Latitude, cache.context)
|
|
secondAngle := occultationRiseSetHorizonAngle(tt, second.Longitude, second.Latitude, cache.context)
|
|
if !finite(firstAngle) || !finite(secondAngle) {
|
|
return nil, false
|
|
}
|
|
centerAngle := riseSetNormalizeRadians(
|
|
firstAngle + math.Remainder(secondAngle-firstAngle, 2*math.Pi)/2,
|
|
)
|
|
result := make(map[RiseSetPhase]OccultationPathPoint, 2)
|
|
for _, phase := range []RiseSetPhase{RiseSetPhaseStart, RiseSetPhaseEnd} {
|
|
found := false
|
|
for scale := 0; scale < 22 && !found; scale++ {
|
|
delta := 0.0
|
|
if scale > 0 {
|
|
delta = math.Ldexp(1e-6, scale-1)
|
|
}
|
|
for _, sign := range []float64{-1, 1} {
|
|
if delta == 0 && sign > 0 {
|
|
continue
|
|
}
|
|
point, ok := occultationRiseSetPhasePointOnHorizon(
|
|
tt, centerAngle+sign*delta, phase, direction, location, cache,
|
|
)
|
|
if !ok || math.Min(
|
|
occultationPathDistanceKM(first, point),
|
|
occultationPathDistanceKM(second, point),
|
|
) > 500 {
|
|
continue
|
|
}
|
|
result[phase], found = point, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
return result, len(result) > 0
|
|
}
|
|
|
|
func occultationRiseSetFoldBridgesPhaseJunction(
|
|
junction OccultationPathPoint,
|
|
endpoint occultationRiseSetEndpoint,
|
|
fold OccultationPathPoint,
|
|
direction RiseSetDirection,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) bool {
|
|
junctionTT := occultationTimeToTT(junction.Time)
|
|
endpointTT := occultationTimeToTT(endpoint.point.Time)
|
|
foldTT := occultationTimeToTT(fold.Time)
|
|
const timeToleranceDays = 1e-8
|
|
if endpoint.atStart {
|
|
if foldTT > math.Min(junctionTT, endpointTT)+timeToleranceDays {
|
|
return false
|
|
}
|
|
} else if foldTT < math.Max(junctionTT, endpointTT)-timeToleranceDays {
|
|
return false
|
|
}
|
|
if occultationPathDistanceKM(fold, junction) > 3000 ||
|
|
occultationPathDistanceKM(fold, endpoint.point) > 3000 {
|
|
return false
|
|
}
|
|
evaluation := cache.evaluation(foldTT)
|
|
_, key, valid := evaluation.classify(fold.Longitude, fold.Latitude, true, fold.Time.Location())
|
|
return valid && key.phase == RiseSetPhaseGreatest && key.direction == direction
|
|
}
|
|
|
|
func occultationRiseSetMidpoint(first, second OccultationPathPoint) OccultationPathPoint {
|
|
firstTT := occultationTimeToTT(first.Time)
|
|
secondTT := occultationTimeToTT(second.Time)
|
|
return OccultationPathPoint{
|
|
Time: occultationTTToLocation((firstTT+secondTT)/2, first.Time.Location()),
|
|
Longitude: normalizeLongitude(
|
|
first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2,
|
|
),
|
|
Latitude: (first.Latitude + second.Latitude) / 2,
|
|
MoonAltitude: (first.MoonAltitude + second.MoonAltitude) / 2,
|
|
}
|
|
}
|
|
|
|
func completeOccultationRiseSetDirectionJunctions(
|
|
curves []OccultationRiseSetCurve,
|
|
stepDays float64,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) {
|
|
type candidate struct {
|
|
riseIndex, setIndex int
|
|
junction OccultationPathPoint
|
|
riseAttachment occultationRiseSetPhaseAttachment
|
|
setAttachment occultationRiseSetPhaseAttachment
|
|
metric float64
|
|
}
|
|
curveIndices := make(map[occultationRiseSetCurveKey]int, len(curves))
|
|
for index, curve := range curves {
|
|
curveIndices[occultationRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
|
|
}
|
|
for _, phase := range []RiseSetPhase{RiseSetPhaseStart, RiseSetPhaseGreatest, RiseSetPhaseEnd} {
|
|
riseIndex, haveRise := curveIndices[occultationRiseSetCurveKey{phase: phase, direction: RiseSetDirectionRise}]
|
|
setIndex, haveSet := curveIndices[occultationRiseSetCurveKey{phase: phase, direction: RiseSetDirectionSet}]
|
|
if !haveRise || !haveSet {
|
|
continue
|
|
}
|
|
riseEndpoints := occultationRiseSetAllEndpoints(curves[riseIndex].Segments)
|
|
setEndpoints := occultationRiseSetAllEndpoints(curves[setIndex].Segments)
|
|
candidates := make([]candidate, 0, len(riseEndpoints)*len(setEndpoints))
|
|
for riseEndpointIndex, riseEndpoint := range riseEndpoints {
|
|
for setEndpointIndex, setEndpoint := range setEndpoints {
|
|
deltaDays := math.Abs(occultationTimeToTT(riseEndpoint.point.Time) - occultationTimeToTT(setEndpoint.point.Time))
|
|
if deltaDays > 3*stepDays {
|
|
continue
|
|
}
|
|
distance := occultationPathDistanceKM(riseEndpoint.point, setEndpoint.point)
|
|
if distance > 3000 {
|
|
continue
|
|
}
|
|
junction, ok := refineOccultationRiseSetDirectionJunctionOnHorizon(
|
|
riseEndpoint.point, setEndpoint.point, phase == RiseSetPhaseGreatest, location, cache,
|
|
)
|
|
if !ok {
|
|
longitude := normalizeLongitude(riseEndpoint.point.Longitude + math.Remainder(setEndpoint.point.Longitude-riseEndpoint.point.Longitude, 360)/2)
|
|
latitude := (riseEndpoint.point.Latitude + setEndpoint.point.Latitude) / 2
|
|
junction, ok = refineOccultationRiseSetDirectionJunction(
|
|
(occultationTimeToTT(riseEndpoint.point.Time)+occultationTimeToTT(setEndpoint.point.Time))/2,
|
|
longitude, latitude, phase == RiseSetPhaseGreatest, location, cache,
|
|
)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
junctionDeltaRise := math.Abs(occultationTimeToTT(junction.Time) - occultationTimeToTT(riseEndpoint.point.Time))
|
|
junctionDeltaSet := math.Abs(occultationTimeToTT(junction.Time) - occultationTimeToTT(setEndpoint.point.Time))
|
|
junctionDistanceRise := occultationPathDistanceKM(junction, riseEndpoint.point)
|
|
junctionDistanceSet := occultationPathDistanceKM(junction, setEndpoint.point)
|
|
if !occultationRiseSetEndpointAcceptsJunction(riseEndpoint, junction) ||
|
|
!occultationRiseSetEndpointAcceptsJunction(setEndpoint, junction) ||
|
|
junctionDeltaRise > 3*stepDays || junctionDeltaSet > 3*stepDays ||
|
|
junctionDistanceRise > 3000 || junctionDistanceSet > 3000 ||
|
|
occultationRiseSetBranchChanged(junctionDistanceRise, junctionDeltaRise) ||
|
|
occultationRiseSetBranchChanged(junctionDistanceSet, junctionDeltaSet) {
|
|
continue
|
|
}
|
|
riseAttachment, riseOK := occultationRiseSetDirectionAttachment(
|
|
riseEndpoint, junction, phase, RiseSetDirectionRise,
|
|
stepDays, location, cache,
|
|
)
|
|
setAttachment, setOK := occultationRiseSetDirectionAttachment(
|
|
setEndpoint, junction, phase, RiseSetDirectionSet,
|
|
stepDays, location, cache,
|
|
)
|
|
if !riseOK || !setOK {
|
|
continue
|
|
}
|
|
metric := occultationPathDistanceKM(junction, riseEndpoint.point) +
|
|
occultationPathDistanceKM(junction, setEndpoint.point)
|
|
candidates = append(candidates, candidate{
|
|
riseIndex: riseEndpointIndex, setIndex: setEndpointIndex,
|
|
junction: junction, riseAttachment: riseAttachment,
|
|
setAttachment: setAttachment, metric: metric,
|
|
})
|
|
}
|
|
}
|
|
sort.SliceStable(candidates, func(first, second int) bool {
|
|
return candidates[first].metric < candidates[second].metric
|
|
})
|
|
usedRise := make([]bool, len(riseEndpoints))
|
|
usedSet := make([]bool, len(setEndpoints))
|
|
for _, candidate := range candidates {
|
|
if usedRise[candidate.riseIndex] || usedSet[candidate.setIndex] {
|
|
continue
|
|
}
|
|
occultationRiseSetAttachDirectionJunction(
|
|
&curves[riseIndex], candidate.riseAttachment, candidate.junction,
|
|
)
|
|
occultationRiseSetAttachDirectionJunction(
|
|
&curves[setIndex], candidate.setAttachment, candidate.junction,
|
|
)
|
|
usedRise[candidate.riseIndex] = true
|
|
usedSet[candidate.setIndex] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func occultationRiseSetDirectionAttachment(
|
|
endpoint occultationRiseSetEndpoint,
|
|
junction OccultationPathPoint,
|
|
phase RiseSetPhase,
|
|
direction RiseSetDirection,
|
|
stepDays float64,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (occultationRiseSetPhaseAttachment, bool) {
|
|
attachment := occultationRiseSetPhaseAttachment{endpoint: endpoint}
|
|
if occultationPathDistanceKM(junction, endpoint.point) <= occultationPathBoundarySpacingKM ||
|
|
occultationRiseSetPhaseSegmentIsContinuous(
|
|
junction, endpoint.point, phase, direction, location, cache,
|
|
) {
|
|
return attachment, true
|
|
}
|
|
fold, ok := refineOccultationRiseSetFold(
|
|
junction, endpoint.point, endpoint.atStart,
|
|
phase == RiseSetPhaseGreatest, stepDays, location, cache,
|
|
)
|
|
if !ok || !occultationRiseSetFoldBridgesDirectionJunction(
|
|
junction, endpoint, fold, phase, direction, cache,
|
|
) {
|
|
return occultationRiseSetPhaseAttachment{}, false
|
|
}
|
|
attachment.fold, attachment.hasFold = fold, true
|
|
return attachment, true
|
|
}
|
|
|
|
func occultationRiseSetAttachDirectionJunction(
|
|
curve *OccultationRiseSetCurve,
|
|
attachment occultationRiseSetPhaseAttachment,
|
|
junction OccultationPathPoint,
|
|
) {
|
|
endpoint := attachment.endpoint
|
|
if attachment.hasFold {
|
|
curve.Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
|
|
curve.Segments[endpoint.segmentIndex], attachment.fold, endpoint.atStart,
|
|
)
|
|
shortBranch := []OccultationPathPoint{attachment.fold, junction}
|
|
if !endpoint.atStart {
|
|
shortBranch[0], shortBranch[1] = shortBranch[1], shortBranch[0]
|
|
}
|
|
curve.Segments = append(curve.Segments, shortBranch)
|
|
return
|
|
}
|
|
curve.Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
|
|
curve.Segments[endpoint.segmentIndex], junction, endpoint.atStart,
|
|
)
|
|
}
|
|
|
|
func occultationRiseSetFoldBridgesDirectionJunction(
|
|
junction OccultationPathPoint,
|
|
endpoint occultationRiseSetEndpoint,
|
|
fold OccultationPathPoint,
|
|
phase RiseSetPhase,
|
|
direction RiseSetDirection,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) bool {
|
|
junctionTT := occultationTimeToTT(junction.Time)
|
|
endpointTT := occultationTimeToTT(endpoint.point.Time)
|
|
foldTT := occultationTimeToTT(fold.Time)
|
|
if endpoint.atStart {
|
|
if foldTT > math.Min(junctionTT, endpointTT)+occultationRiseSetTimeEpsilonDays {
|
|
return false
|
|
}
|
|
} else if foldTT < math.Max(junctionTT, endpointTT)-occultationRiseSetTimeEpsilonDays {
|
|
return false
|
|
}
|
|
if occultationPathDistanceKM(fold, junction) > 3000 ||
|
|
occultationPathDistanceKM(fold, endpoint.point) > 3000 {
|
|
return false
|
|
}
|
|
evaluation := cache.evaluation(foldTT)
|
|
_, key, valid := evaluation.classify(
|
|
fold.Longitude, fold.Latitude, phase == RiseSetPhaseGreatest, fold.Time.Location(),
|
|
)
|
|
return valid && key.phase == phase && key.direction == direction
|
|
}
|
|
|
|
func occultationRiseSetEndpointAcceptsJunction(
|
|
endpoint occultationRiseSetEndpoint,
|
|
junction OccultationPathPoint,
|
|
) bool {
|
|
endpointTT := occultationTimeToTT(endpoint.point.Time)
|
|
junctionTT := occultationTimeToTT(junction.Time)
|
|
if endpoint.atStart {
|
|
return junctionTT <= endpointTT+occultationRiseSetTimeEpsilonDays
|
|
}
|
|
return junctionTT >= endpointTT-occultationRiseSetTimeEpsilonDays
|
|
}
|
|
|
|
func occultationRiseSetUnsharedEndpoints(curveIndex int, segments [][]OccultationPathPoint) []occultationRiseSetEndpoint {
|
|
endpoints := occultationRiseSetAllEndpoints(segments)
|
|
result := make([]occultationRiseSetEndpoint, 0, len(endpoints))
|
|
for index, endpoint := range endpoints {
|
|
shared := false
|
|
for otherIndex, other := range endpoints {
|
|
if index == otherIndex || endpoint.segmentIndex == other.segmentIndex {
|
|
continue
|
|
}
|
|
if math.Abs(occultationTimeToTT(endpoint.point.Time)-occultationTimeToTT(other.point.Time)) <= 1e-8 &&
|
|
occultationPathDistanceKM(endpoint.point, other.point) <= 0.01 {
|
|
shared = true
|
|
break
|
|
}
|
|
}
|
|
if !shared {
|
|
endpoint.curveIndex = curveIndex
|
|
result = append(result, endpoint)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func occultationRiseSetAllEndpoints(segments [][]OccultationPathPoint) []occultationRiseSetEndpoint {
|
|
endpoints := make([]occultationRiseSetEndpoint, 0, 2*len(segments))
|
|
for segmentIndex, segment := range segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
endpoints = append(endpoints, occultationRiseSetEndpoint{
|
|
segmentIndex: segmentIndex, atStart: atStart,
|
|
point: occultationRiseSetSegmentEndpoint(segment, atStart),
|
|
})
|
|
}
|
|
}
|
|
return endpoints
|
|
}
|
|
|
|
func occultationRiseSetClosestEndpoint(
|
|
junction OccultationPathPoint,
|
|
segments [][]OccultationPathPoint,
|
|
stepDays float64,
|
|
) (occultationRiseSetEndpoint, bool) {
|
|
best := occultationRiseSetEndpoint{}
|
|
bestMetric := math.Inf(1)
|
|
junctionTT := occultationTimeToTT(junction.Time)
|
|
for segmentIndex, segment := range segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
point := occultationRiseSetSegmentEndpoint(segment, atStart)
|
|
pointTT := occultationTimeToTT(point.Time)
|
|
if atStart && junctionTT > pointTT+1e-8 || !atStart && junctionTT < pointTT-1e-8 ||
|
|
math.Abs(junctionTT-pointTT) > 3*stepDays || occultationPathDistanceKM(junction, point) > 3000 {
|
|
continue
|
|
}
|
|
metric := occultationPathDistanceKM(junction, point) + math.Abs(junctionTT-pointTT)*8640
|
|
if metric < bestMetric {
|
|
best = occultationRiseSetEndpoint{segmentIndex: segmentIndex, atStart: atStart, point: point}
|
|
bestMetric = metric
|
|
}
|
|
}
|
|
}
|
|
return best, bestMetric < math.Inf(1)
|
|
}
|
|
|
|
func refineOccultationRiseSetPhaseJunction(
|
|
seed OccultationPathPoint,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (OccultationPathPoint, bool) {
|
|
coordinates := [3]float64{seed.Longitude, seed.Latitude, occultationTimeToTT(seed.Time)}
|
|
for iteration := 0; iteration < 24; iteration++ {
|
|
evaluation := cache.evaluation(coordinates[2])
|
|
residual, ok := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0], coordinates[1])
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= occultationRiseSetJunctionDerivativeTolerance && math.Abs(residual[2]) <= 1e-8 {
|
|
break
|
|
}
|
|
matrix, ok := occultationRiseSetPhaseJunctionJacobian(coordinates, residual, cache)
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -residual[2]})
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
occultationRiseSetLimitJunctionDelta(&delta)
|
|
coordinates[0] = normalizeLongitude(coordinates[0] + delta[0])
|
|
coordinates[1] += delta[1]
|
|
coordinates[2] += delta[2]
|
|
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
}
|
|
evaluation := cache.evaluation(coordinates[2])
|
|
residual, ok := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0], coordinates[1])
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > occultationRiseSetJunctionDerivativeTolerance || math.Abs(residual[2]) > 1e-7 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
state := evaluation.center.stateAt(coordinates[0], coordinates[1])
|
|
return OccultationPathPoint{Time: occultationTTToLocation(coordinates[2], location), Longitude: coordinates[0], Latitude: coordinates[1], MoonAltitude: state.moonAltitude}, true
|
|
}
|
|
|
|
func refineOccultationRiseSetPhaseJunctionOnHorizon(
|
|
seed OccultationPathPoint,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (OccultationPathPoint, bool) {
|
|
seedTT := occultationTimeToTT(seed.Time)
|
|
coordinates := [2]float64{
|
|
occultationRiseSetHorizonAngle(seedTT, seed.Longitude, seed.Latitude, cache.context),
|
|
0,
|
|
}
|
|
residualAt := func(value [2]float64) ([2]float64, float64, float64, float64, bool) {
|
|
tt := seedTT + value[1]/1440
|
|
longitude, latitude, horizonOK := occultationRiseSetHorizonPoint(tt, value[0], cache.context)
|
|
if !horizonOK {
|
|
return [2]float64{}, 0, 0, 0, false
|
|
}
|
|
evaluation := cache.evaluation(tt)
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
residual := [2]float64{state.contactMetric, evaluation.contactDerivative(longitude, latitude)}
|
|
return residual, tt, longitude, latitude,
|
|
state.valid && finite(residual[0]) && finite(residual[1])
|
|
}
|
|
const angleStep = 1e-4
|
|
const timeStepMinutes = 1.0 / 60.0
|
|
for iteration := 0; iteration < 32; iteration++ {
|
|
residual, _, _, _, ok := residualAt(coordinates)
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= occultationRiseSetJunctionDerivativeTolerance {
|
|
break
|
|
}
|
|
plusAngle, _, _, _, plusAngleOK := residualAt([2]float64{coordinates[0] + angleStep, coordinates[1]})
|
|
minusAngle, _, _, _, minusAngleOK := residualAt([2]float64{coordinates[0] - angleStep, coordinates[1]})
|
|
plusTime, _, _, _, plusTimeOK := residualAt([2]float64{coordinates[0], coordinates[1] + timeStepMinutes})
|
|
minusTime, _, _, _, minusTimeOK := residualAt([2]float64{coordinates[0], coordinates[1] - timeStepMinutes})
|
|
if !plusAngleOK || !minusAngleOK || !plusTimeOK || !minusTimeOK {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
matrix := [2][2]float64{
|
|
{(plusAngle[0] - minusAngle[0]) / (2 * angleStep), (plusTime[0] - minusTime[0]) / (2 * timeStepMinutes)},
|
|
{(plusAngle[1] - minusAngle[1]) / (2 * angleStep), (plusTime[1] - minusTime[1]) / (2 * timeStepMinutes)},
|
|
}
|
|
determinant := matrix[0][0]*matrix[1][1] - matrix[0][1]*matrix[1][0]
|
|
if !finite(determinant) || math.Abs(determinant) < 1e-18 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
delta := [2]float64{
|
|
(-residual[0]*matrix[1][1] + matrix[0][1]*residual[1]) / determinant,
|
|
(-matrix[0][0]*residual[1] + residual[0]*matrix[1][0]) / determinant,
|
|
}
|
|
if math.Abs(delta[0]) > 0.25 {
|
|
delta[0] = math.Copysign(0.25, delta[0])
|
|
}
|
|
if math.Abs(delta[1]) > 5 {
|
|
delta[1] = math.Copysign(5, delta[1])
|
|
}
|
|
coordinates[0] = riseSetNormalizeRadians(coordinates[0] + delta[0])
|
|
coordinates[1] += delta[1]
|
|
}
|
|
residual, tt, longitude, latitude, ok := residualAt(coordinates)
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > occultationRiseSetJunctionDerivativeTolerance {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
state := cache.context(tt).stateAt(longitude, latitude)
|
|
return OccultationPathPoint{
|
|
Time: occultationTTToLocation(tt, location), Longitude: longitude,
|
|
Latitude: latitude, MoonAltitude: state.moonAltitude,
|
|
}, true
|
|
}
|
|
|
|
func refineOccultationRiseSetDirectionJunction(
|
|
tt, longitude, latitude float64,
|
|
greatest bool,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (OccultationPathPoint, bool) {
|
|
coordinates := [3]float64{longitude, latitude, tt}
|
|
for iteration := 0; iteration < 24; iteration++ {
|
|
evaluation := cache.evaluation(coordinates[2])
|
|
residual, ok := occultationRiseSetDirectionJunctionResidualAt(evaluation, coordinates[0], coordinates[1], greatest)
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= 1e-9 && math.Abs(residual[2]) <= 1e-7 {
|
|
break
|
|
}
|
|
matrix, ok := occultationRiseSetDirectionJunctionJacobian(coordinates, residual, greatest, cache)
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -residual[2]})
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
occultationRiseSetLimitJunctionDelta(&delta)
|
|
coordinates[0] = normalizeLongitude(coordinates[0] + delta[0])
|
|
coordinates[1] += delta[1]
|
|
coordinates[2] += delta[2]
|
|
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
}
|
|
evaluation := cache.evaluation(coordinates[2])
|
|
residual, ok := occultationRiseSetDirectionJunctionResidualAt(evaluation, coordinates[0], coordinates[1], greatest)
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-8 || math.Abs(residual[2]) > 1e-7 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
state := evaluation.center.stateAt(coordinates[0], coordinates[1])
|
|
return OccultationPathPoint{Time: occultationTTToLocation(coordinates[2], location), Longitude: coordinates[0], Latitude: coordinates[1], MoonAltitude: state.moonAltitude}, true
|
|
}
|
|
|
|
func refineOccultationRiseSetDirectionJunctionOnHorizon(
|
|
first, second OccultationPathPoint,
|
|
greatest bool,
|
|
location *time.Location,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) (OccultationPathPoint, bool) {
|
|
seedTT := (occultationTimeToTT(first.Time) + occultationTimeToTT(second.Time)) / 2
|
|
firstAngle := occultationRiseSetHorizonAngle(seedTT, first.Longitude, first.Latitude, cache.context)
|
|
secondAngle := occultationRiseSetHorizonAngle(seedTT, second.Longitude, second.Latitude, cache.context)
|
|
if !finite(firstAngle) || !finite(secondAngle) {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
coordinates := [2]float64{
|
|
riseSetNormalizeRadians(firstAngle + math.Remainder(secondAngle-firstAngle, 2*math.Pi)/2),
|
|
0,
|
|
}
|
|
residualAt := func(value [2]float64) ([2]float64, float64, float64, float64, bool) {
|
|
tt := seedTT + value[1]/1440
|
|
longitude, latitude, ok := occultationRiseSetHorizonPoint(tt, value[0], cache.context)
|
|
if !ok {
|
|
return [2]float64{}, 0, 0, 0, false
|
|
}
|
|
evaluation := cache.evaluation(tt)
|
|
phaseResidual, phaseOK := occultationRiseSetPhaseResidual(evaluation, longitude, latitude, greatest)
|
|
altitudeDerivative := evaluation.moonAltitudeDerivative(longitude, latitude)
|
|
return [2]float64{phaseResidual, altitudeDerivative}, tt, longitude, latitude,
|
|
phaseOK && finite(phaseResidual) && finite(altitudeDerivative)
|
|
}
|
|
const (
|
|
angleStep = 1e-4
|
|
timeStepMinute = 1.0 / 60
|
|
)
|
|
for iteration := 0; iteration < 32; iteration++ {
|
|
residual, _, _, _, ok := residualAt(coordinates)
|
|
if !ok {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= 1e-7 {
|
|
break
|
|
}
|
|
anglePlus, _, _, _, anglePlusOK := residualAt([2]float64{coordinates[0] + angleStep, coordinates[1]})
|
|
angleMinus, _, _, _, angleMinusOK := residualAt([2]float64{coordinates[0] - angleStep, coordinates[1]})
|
|
timePlus, _, _, _, timePlusOK := residualAt([2]float64{coordinates[0], coordinates[1] + timeStepMinute})
|
|
timeMinus, _, _, _, timeMinusOK := residualAt([2]float64{coordinates[0], coordinates[1] - timeStepMinute})
|
|
if !anglePlusOK || !angleMinusOK || !timePlusOK || !timeMinusOK {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
matrix := [2][2]float64{
|
|
{(anglePlus[0] - angleMinus[0]) / (2 * angleStep), (timePlus[0] - timeMinus[0]) / (2 * timeStepMinute)},
|
|
{(anglePlus[1] - angleMinus[1]) / (2 * angleStep), (timePlus[1] - timeMinus[1]) / (2 * timeStepMinute)},
|
|
}
|
|
determinant := matrix[0][0]*matrix[1][1] - matrix[0][1]*matrix[1][0]
|
|
if !finite(determinant) || math.Abs(determinant) < 1e-18 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
delta := [2]float64{
|
|
(-residual[0]*matrix[1][1] + matrix[0][1]*residual[1]) / determinant,
|
|
(-matrix[0][0]*residual[1] + residual[0]*matrix[1][0]) / determinant,
|
|
}
|
|
if math.Abs(delta[0]) > 0.25 {
|
|
delta[0] = math.Copysign(0.25, delta[0])
|
|
}
|
|
if math.Abs(delta[1]) > 5 {
|
|
delta[1] = math.Copysign(5, delta[1])
|
|
}
|
|
coordinates[0] = riseSetNormalizeRadians(coordinates[0] + delta[0])
|
|
coordinates[1] += delta[1]
|
|
}
|
|
residual, tt, longitude, latitude, ok := residualAt(coordinates)
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-7 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
state := cache.context(tt).stateAt(longitude, latitude)
|
|
if !state.valid || math.Abs(state.moonAltitude) > 1e-7 {
|
|
return OccultationPathPoint{}, false
|
|
}
|
|
return OccultationPathPoint{
|
|
Time: occultationTTToLocation(tt, location), Longitude: longitude,
|
|
Latitude: latitude, MoonAltitude: state.moonAltitude,
|
|
}, true
|
|
}
|
|
|
|
func occultationRiseSetPhaseJunctionResidualAt(evaluation occultationRiseSetEvaluation, longitude, latitude float64) ([3]float64, bool) {
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
residual := [3]float64{state.contactMetric, evaluation.contactDerivative(longitude, latitude), state.moonAltitude}
|
|
return residual, state.valid && finite(residual[1]) && finite(residual[2])
|
|
}
|
|
|
|
func occultationRiseSetDirectionJunctionResidualAt(evaluation occultationRiseSetEvaluation, longitude, latitude float64, greatest bool) ([3]float64, bool) {
|
|
phase, phaseOK := occultationRiseSetPhaseResidual(evaluation, longitude, latitude, greatest)
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
residual := [3]float64{phase, state.moonAltitude, evaluation.moonAltitudeDerivative(longitude, latitude)}
|
|
return residual, phaseOK && state.valid && finite(residual[1]) && finite(residual[2])
|
|
}
|
|
|
|
func occultationRiseSetPhaseResidual(evaluation occultationRiseSetEvaluation, longitude, latitude float64, greatest bool) (float64, bool) {
|
|
if greatest {
|
|
value := evaluation.separationDerivative(longitude, latitude)
|
|
return value, finite(value)
|
|
}
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
return state.contactMetric, state.valid && finite(state.contactMetric)
|
|
}
|
|
|
|
func occultationRiseSetPhaseJunctionJacobian(
|
|
coordinates [3]float64,
|
|
residual [3]float64,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) ([3][3]float64, bool) {
|
|
const geographicStep = 1e-4
|
|
const timeStep = 1.0 / 86400.0
|
|
evaluation := cache.evaluation(coordinates[2])
|
|
plusLongitude, longitudeOK := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0]+geographicStep, coordinates[1])
|
|
plusLatitude, latitudeOK := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0], coordinates[1]+geographicStep)
|
|
plusTimeEval := cache.evaluation(coordinates[2] + timeStep)
|
|
plusTime, timeOK := occultationRiseSetPhaseJunctionResidualAt(plusTimeEval, coordinates[0], coordinates[1])
|
|
if !longitudeOK || !latitudeOK || !timeOK {
|
|
return [3][3]float64{}, false
|
|
}
|
|
var matrix [3][3]float64
|
|
for row := range matrix {
|
|
matrix[row][0] = (plusLongitude[row] - residual[row]) / geographicStep
|
|
matrix[row][1] = (plusLatitude[row] - residual[row]) / geographicStep
|
|
matrix[row][2] = (plusTime[row] - residual[row]) / timeStep
|
|
}
|
|
return matrix, true
|
|
}
|
|
|
|
func occultationRiseSetDirectionJunctionJacobian(
|
|
coordinates [3]float64,
|
|
residual [3]float64,
|
|
greatest bool,
|
|
cache *occultationRiseSetEvaluationCache,
|
|
) ([3][3]float64, bool) {
|
|
const geographicStep = 1e-4
|
|
const timeStep = 1.0 / 86400.0
|
|
evaluation := cache.evaluation(coordinates[2])
|
|
plusLongitude, longitudeOK := occultationRiseSetDirectionJunctionResidualAt(
|
|
evaluation, coordinates[0]+geographicStep, coordinates[1], greatest,
|
|
)
|
|
plusLatitude, latitudeOK := occultationRiseSetDirectionJunctionResidualAt(
|
|
evaluation, coordinates[0], coordinates[1]+geographicStep, greatest,
|
|
)
|
|
plusTimeEval := cache.evaluation(coordinates[2] + timeStep)
|
|
plusTime, timeOK := occultationRiseSetDirectionJunctionResidualAt(
|
|
plusTimeEval, coordinates[0], coordinates[1], greatest,
|
|
)
|
|
if !longitudeOK || !latitudeOK || !timeOK {
|
|
return [3][3]float64{}, false
|
|
}
|
|
var matrix [3][3]float64
|
|
for row := range matrix {
|
|
matrix[row][0] = (plusLongitude[row] - residual[row]) / geographicStep
|
|
matrix[row][1] = (plusLatitude[row] - residual[row]) / geographicStep
|
|
matrix[row][2] = (plusTime[row] - residual[row]) / timeStep
|
|
}
|
|
return matrix, true
|
|
}
|
|
|
|
func occultationRiseSetLimitJunctionDelta(delta *[3]float64) {
|
|
geographicScale := math.Max(math.Abs(delta[0]), math.Abs(delta[1]))
|
|
if geographicScale > 2 {
|
|
delta[0] *= 2 / geographicScale
|
|
delta[1] *= 2 / geographicScale
|
|
}
|
|
if math.Abs(delta[2]) > 5.0/1440 {
|
|
delta[2] = math.Copysign(5.0/1440, delta[2])
|
|
}
|
|
}
|
|
|
|
func occultationRiseSetSegmentEndpoint(segment []OccultationPathPoint, atStart bool) OccultationPathPoint {
|
|
if atStart {
|
|
return segment[0]
|
|
}
|
|
return segment[len(segment)-1]
|
|
}
|
|
|
|
func occultationRiseSetAddEndpoint(segment []OccultationPathPoint, point OccultationPathPoint, atStart bool) []OccultationPathPoint {
|
|
current := occultationRiseSetSegmentEndpoint(segment, atStart)
|
|
if math.Abs(occultationTimeToTT(current.Time)-occultationTimeToTT(point.Time)) <= occultationRiseSetTimeEpsilonDays && occultationPathDistanceKM(current, point) <= 0.01 {
|
|
if atStart {
|
|
segment[0] = point
|
|
} else {
|
|
segment[len(segment)-1] = point
|
|
}
|
|
return segment
|
|
}
|
|
if atStart {
|
|
return append([]OccultationPathPoint{point}, segment...)
|
|
}
|
|
return append(segment, point)
|
|
}
|