16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
1588 lines
57 KiB
Go
1588 lines
57 KiB
Go
package basic
|
||
|
||
import (
|
||
"math"
|
||
"sort"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
// occultationPathThetaIntervalCacheMaximumEntries 限制每个 frame 记忆的中心角数量;一次求解只会用到个位数个。
|
||
// occultationPathThetaIntervalCacheMaximumEntries bounds the memory of center angles per frame; a solve uses only a handful.
|
||
occultationPathThetaIntervalCacheMaximumEntries = 8
|
||
occultationPathDefaultStepDays = 1.0 / 1440.0
|
||
occultationPathMinStepDays = 1.0 / 86400.0
|
||
occultationPathMaxSampleCount = 30000
|
||
occultationPathMaxAdaptiveDepth = 20
|
||
occultationPathBoundarySpacingKM = 500.0
|
||
occultationPathContourSpacingKM = 40.0
|
||
occultationPathBoundaryMergeKM = 1.0
|
||
occultationPathVelocityStepDays = 1.0 / 1440.0
|
||
occultationPathBoundaryScanPoints = 720
|
||
occultationPathRootToleranceDays = occultationEventSelectionToleranceDays
|
||
occultationPathRangeStepDays = 5.0 / 1440.0
|
||
occultationPathSearchSpanDays = 2.0
|
||
occultationPathWidthToleranceKM = 0.005
|
||
occultationPathMaxOutputPointCount = 2000000
|
||
occultationPathFootprintPointBudget = 2048
|
||
occultationPathBoundaryBranchJumpKM = 120.0
|
||
occultationPathBoundaryBranchSpeedKMPerSecond = 10.0
|
||
|
||
occultationPathEarthEquatorialRadiusKM = 6378.1366
|
||
occultationPathEarthPolarRatio = 0.99664719
|
||
occultationPathAstronomicalUnitKM = 149597870.7
|
||
)
|
||
|
||
// FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带。
|
||
// 查询窗口按全球几何掩甚点选择事件,端点容差为 10 ms,与数值根精度一致。返回路径扩展到完整全球起止点;函数不会加载内嵌星表,调用者需显式提供坐标。
|
||
// FindStarOccultationPaths searches the global lunar-occultation footprint of one point-source star.
|
||
// The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end; the function does not load the embedded catalog.
|
||
func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) {
|
||
if err := validateOccultationTimeRange(start, end); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := star.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := options.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
options = normalizeOccultationPathOptions(options)
|
||
startTT := occultationTimeToTT(start)
|
||
endTT := occultationTimeToTT(end)
|
||
candidateStartTT := startTT - occultationPathSearchSpanDays
|
||
candidateEndTT := endTT + occultationPathSearchSpanDays
|
||
coarseOptions := OccultationSearchOptions{}
|
||
candidates := starOccultationGeocentricCandidateGreatestTimes(
|
||
candidateStartTT,
|
||
candidateEndTT,
|
||
starOccultationCoarseStepDays(coarseOptions),
|
||
star,
|
||
0,
|
||
)
|
||
paths := make([]StarOccultationPath, 0, len(candidates))
|
||
for _, seedTT := range candidates {
|
||
path, ok, err := starOccultationPathAtSeed(seedTT, star, options, start, end)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !ok {
|
||
continue
|
||
}
|
||
if len(paths) > 0 && math.Abs(paths[len(paths)-1].Greatest.Time.Sub(path.Greatest.Time).Seconds()) <= 60 {
|
||
continue
|
||
}
|
||
paths = append(paths, path)
|
||
}
|
||
sort.SliceStable(paths, func(i, j int) bool {
|
||
return paths[i].Greatest.Time.Before(paths[j].Greatest.Time)
|
||
})
|
||
return paths, nil
|
||
}
|
||
|
||
func normalizeOccultationPathOptions(options OccultationPathOptions) OccultationPathOptions {
|
||
if options.Algorithm == "" {
|
||
options.Algorithm = OccultationPathAlgorithmOptimized
|
||
}
|
||
if options.Step <= 0 {
|
||
options.Step = time.Duration(occultationPathDefaultStepDays * float64(24*time.Hour))
|
||
}
|
||
if float64(options.Step)/float64(24*time.Hour) < occultationPathMinStepDays {
|
||
options.Step = time.Second
|
||
}
|
||
if options.TargetSpacingKM <= 0 || math.IsNaN(options.TargetSpacingKM) || math.IsInf(options.TargetSpacingKM, 0) {
|
||
options.TargetSpacingKM = 0
|
||
}
|
||
if len(options.GreatestTimeValues) > 0 {
|
||
values := make([]float64, 0, len(options.GreatestTimeValues))
|
||
for _, value := range options.GreatestTimeValues {
|
||
if !finite(value) {
|
||
continue
|
||
}
|
||
duplicate := false
|
||
for _, existing := range values {
|
||
if math.Abs(existing-value) <= 1e-9 {
|
||
duplicate = true
|
||
break
|
||
}
|
||
}
|
||
if !duplicate {
|
||
values = append(values, value)
|
||
}
|
||
}
|
||
sort.Float64s(values)
|
||
options.GreatestTimeValues = values
|
||
}
|
||
return options
|
||
}
|
||
|
||
func starOccultationPathAtSeed(
|
||
seedTT float64,
|
||
star StarCoordinate,
|
||
options OccultationPathOptions,
|
||
selectionStart, selectionEnd time.Time,
|
||
) (StarOccultationPath, bool, error) {
|
||
location := selectionStart.Location()
|
||
cache := newStarOccultationEventCache(star)
|
||
cache.prepareLocalEphemeris(seedTT)
|
||
frameAt := cache.frameAt
|
||
candidateFrameAt := cache.candidateFrameAt
|
||
searchStart := seedTT - occultationPathSearchSpanDays
|
||
searchEnd := seedTT + occultationPathSearchSpanDays
|
||
outerStart, outerEnd, ok := starOccultationPathWindowWithCandidateFrames(
|
||
seedTT, searchStart, searchEnd, candidateFrameAt, frameAt, false,
|
||
)
|
||
if !ok {
|
||
return StarOccultationPath{}, false, nil
|
||
}
|
||
|
||
centerStart, centerEnd, hasCenter := starOccultationPathWindowWithCandidateFrames(
|
||
seedTT, searchStart, searchEnd, candidateFrameAt, frameAt, true,
|
||
)
|
||
// Global markers retain the same full-term ephemerides as event-only
|
||
// queries. The optimized path cache is selected after these markers.
|
||
greatestTT := starOccultationPathGreatestWithFrame(seedTT, outerStart, outerEnd, frameAt)
|
||
greatest, greatestOK := starOccultationPathCenterPointWithFrame(greatestTT, frameAt, location)
|
||
if !greatestOK {
|
||
if hasCenter {
|
||
greatestTT = math.Max(centerStart, math.Min(centerEnd, greatestTT))
|
||
greatest, greatestOK = starOccultationPathCenterPointWithFrame(greatestTT, frameAt, location)
|
||
}
|
||
}
|
||
if !greatestOK {
|
||
// For a non-central path the shadow axis misses the ellipsoid. Greatest
|
||
// is the nearest point on the ellipsoid to that axis, not an outer
|
||
// contact tangent; the latter is a band edge and shifts the marker.
|
||
greatest, greatestOK = occultationPathTrackPointForFrame(greatestTT, frameAt, location)
|
||
}
|
||
if !greatestOK {
|
||
return StarOccultationPath{}, false, nil
|
||
}
|
||
if !occultationTimeInSelectionWindow(greatest.Time, selectionStart, selectionEnd) {
|
||
return StarOccultationPath{}, false, nil
|
||
}
|
||
if occultationPathEstimatedPointCount(
|
||
outerStart, outerEnd, centerStart, centerEnd, hasCenter,
|
||
0, 0, false, greatestTT, options,
|
||
) > occultationPathMaxOutputPointCount {
|
||
return StarOccultationPath{}, false, ErrOccultationPathSamplingLimit
|
||
}
|
||
|
||
start := starOccultationPathBoundaryEndpointWithFrame(outerStart, frameAt, location, 1)
|
||
end := starOccultationPathBoundaryEndpointWithFrame(outerEnd, frameAt, location, -1)
|
||
if !start.valid || !end.valid {
|
||
return StarOccultationPath{}, false, nil
|
||
}
|
||
exactFrameAt := frameAt
|
||
if options.Algorithm != OccultationPathAlgorithmExact {
|
||
optimized := newStarOccultationEventCache(star)
|
||
optimized.preparePathEphemeris(seedTT, options.Algorithm)
|
||
if optimized.local.dense {
|
||
cache = optimized
|
||
frameAt = cache.frameAt
|
||
}
|
||
}
|
||
|
||
path := StarOccultationPath{
|
||
TargetID: star.ID,
|
||
Start: start.point,
|
||
Greatest: greatest,
|
||
End: end.point,
|
||
Complete: outerStart > searchStart && outerEnd < searchEnd,
|
||
Step: options.Step,
|
||
TargetSpacingKM: options.TargetSpacingKM,
|
||
}
|
||
centerLine, northern, southern, err := starOccultationPathSamplesWithFrame(
|
||
outerStart,
|
||
outerEnd,
|
||
centerStart,
|
||
centerEnd,
|
||
hasCenter,
|
||
greatestTT,
|
||
frameAt,
|
||
options,
|
||
location,
|
||
)
|
||
if err != nil {
|
||
return StarOccultationPath{}, false, err
|
||
}
|
||
if cache.local.dense {
|
||
correctOccultationCenterWidths(centerLine, exactFrameAt)
|
||
}
|
||
path.CenterLine = centerLine
|
||
path.NorthernLimit = occultationPathWithEndpoints(start.point, end.point, northern)
|
||
path.SouthernLimit = occultationPathWithEndpoints(start.point, end.point, southern)
|
||
path.NorthernLimit = occultationStationCorrectLimitSeries(
|
||
path.NorthernLimit, frameAt, cache.riseSetContextAt, false, location,
|
||
)
|
||
path.SouthernLimit = occultationStationCorrectLimitSeries(
|
||
path.SouthernLimit, frameAt, cache.riseSetContextAt, false, location,
|
||
)
|
||
path.GreatestLimitSeparationKM, _ = occultationPathLimitSeparations(
|
||
path.NorthernLimit, path.SouthernLimit, greatestTT,
|
||
)
|
||
if !options.DisableFootprints {
|
||
path.Footprints = planetOccultationFootprints(outerStart, outerEnd, greatestTT, frameAt, options, location)
|
||
}
|
||
path.RiseSetCurves = occultationRiseSetCurvesWithCache(
|
||
outerStart, outerEnd, greatestTT, options, location, cache.riseSetCache,
|
||
)
|
||
path.GreatestTimeContours = occultationGreatestTimeContours(
|
||
occultationGreatestTimeLevels(options, outerStart, outerEnd), outerStart, outerEnd, cache.riseSetCache,
|
||
[][]OccultationPathPoint{path.CenterLine, path.NorthernLimit, path.SouthernLimit},
|
||
false, location,
|
||
)
|
||
bandContourTimes := occultationRiseSetEndpointTimes(path.RiseSetCurves)
|
||
path.BandContours = occultationContactBandContoursWithAdditionalTimes(
|
||
start.point, end.point, outerStart, outerEnd, greatestTT, frameAt, options, location, bandContourTimes,
|
||
)
|
||
if options.DisableFootprints {
|
||
path.BandFootprints = planetOccultationBandFootprints(
|
||
outerStart, outerEnd, greatestTT, frameAt, location, bandContourTimes,
|
||
)
|
||
if options.IncludeFootprintTimeline {
|
||
path.Footprints = planetOccultationTimelineFootprints(
|
||
outerStart, outerEnd, greatestTT, frameAt, options, location,
|
||
)
|
||
}
|
||
}
|
||
if len(path.Footprints) > 0 {
|
||
path.Footprints = occultationStationCorrectFootprintEdges(
|
||
path.Footprints, frameAt, cache.riseSetContextAt, false, location,
|
||
)
|
||
}
|
||
if len(path.BandFootprints) > 0 {
|
||
path.BandFootprints = occultationStationCorrectFootprintEdges(
|
||
path.BandFootprints, frameAt, cache.riseSetContextAt, false, location,
|
||
)
|
||
}
|
||
path.BandContours = occultationStationCorrectContours(
|
||
path.BandContours, path.RiseSetCurves, cache.riseSetCache, location,
|
||
)
|
||
path.VisibilityContours = occultationStationVisibilityEnvelopeContours(
|
||
path.RiseSetCurves, cache.riseSetCache, location,
|
||
)
|
||
return path, true, nil
|
||
}
|
||
|
||
func occultationPathEstimatedPointCount(
|
||
outerStart, outerEnd, centerStart, centerEnd float64,
|
||
hasCenter bool,
|
||
totalStart, totalEnd float64,
|
||
hasTotal bool,
|
||
greatestTT float64,
|
||
options OccultationPathOptions,
|
||
) int {
|
||
stepDays := float64(options.Step) / float64(24*time.Hour)
|
||
if stepDays <= 0 {
|
||
stepDays = occultationPathDefaultStepDays
|
||
}
|
||
estimate := 0
|
||
add := func(value int) {
|
||
estimate = occultationPathAccumulatePointEstimate(estimate, value)
|
||
}
|
||
add(3 * len(occultationPathSampleTimes(outerStart, outerEnd, greatestTT, stepDays)))
|
||
contourStepDays := occultationPathContourStepDays(options)
|
||
add(2 * len(occultationPathSampleTimes(outerStart, outerEnd, greatestTT, contourStepDays)))
|
||
if hasCenter {
|
||
add(3 * len(occultationPathSampleTimes(centerStart, centerEnd, greatestTT, stepDays)))
|
||
}
|
||
if hasTotal {
|
||
add(3 * len(occultationPathSampleTimes(totalStart, totalEnd, greatestTT, stepDays)))
|
||
add(2 * len(occultationPathSampleTimes(totalStart, totalEnd, greatestTT, contourStepDays)))
|
||
}
|
||
footprintStep := planetOccultationFootprintSampleStepDays(options)
|
||
footprintLimit := planetOccultationFootprintMaxSamples
|
||
if options.DisableFootprints && !options.IncludeFootprintTimeline {
|
||
footprintStep = planetOccultationBandSampleStepDays()
|
||
footprintLimit = planetOccultationBandMaxSamples
|
||
}
|
||
add(len(occultationPathSampleTimesWithLimit(outerStart, outerEnd, greatestTT, footprintStep, footprintLimit)) * occultationPathFootprintPointBudget)
|
||
if options.DisableFootprints && options.IncludeFootprintTimeline {
|
||
add(len(planetOccultationBandSampleTimes(
|
||
outerStart, outerEnd, greatestTT, planetOccultationBandSampleStepDays(), planetOccultationBandMaxSamples,
|
||
)) * occultationPathFootprintPointBudget)
|
||
}
|
||
if hasTotal {
|
||
add(len(occultationPathSampleTimesWithLimit(totalStart, totalEnd, greatestTT, footprintStep, footprintLimit)) * occultationPathFootprintPointBudget)
|
||
if options.DisableFootprints && options.IncludeFootprintTimeline {
|
||
add(len(planetOccultationBandSampleTimes(
|
||
totalStart, totalEnd, greatestTT, planetOccultationBandSampleStepDays(), planetOccultationBandMaxSamples,
|
||
)) * occultationPathFootprintPointBudget)
|
||
}
|
||
}
|
||
if !options.DisableRiseSet {
|
||
riseSetStep := 5 * time.Minute
|
||
if options.RiseSetStep > 0 {
|
||
riseSetStep = options.RiseSetStep
|
||
}
|
||
riseSetCount := len(occultationPathSampleTimes(
|
||
outerStart, outerEnd, greatestTT,
|
||
float64(riseSetStep)/float64(24*time.Hour),
|
||
))
|
||
add(6 * riseSetCount)
|
||
}
|
||
return estimate
|
||
}
|
||
|
||
func occultationPathAccumulatePointEstimate(estimate, value int) int {
|
||
if value <= 0 || estimate > occultationPathMaxOutputPointCount {
|
||
return estimate
|
||
}
|
||
if value > occultationPathMaxOutputPointCount-estimate {
|
||
return occultationPathMaxOutputPointCount + 1
|
||
}
|
||
return estimate + value
|
||
}
|
||
|
||
func occultationPathWithEndpoints(start, end OccultationPathPoint, points []OccultationPathPoint) []OccultationPathPoint {
|
||
result := make([]OccultationPathPoint, 0, len(points)+2)
|
||
result = append(result, start)
|
||
for _, point := range points {
|
||
if point.Time.After(result[len(result)-1].Time) && point.Time.Before(end.Time) {
|
||
result = append(result, point)
|
||
}
|
||
}
|
||
return append(result, end)
|
||
}
|
||
|
||
// occultationPathLimitSeparations 在同一时刻的南北限采样对上填写地面间距,并返回最接近掩甚的那一对的间距。
|
||
func occultationPathLimitSeparations(
|
||
north, south []OccultationPathPoint,
|
||
greatestTT float64,
|
||
) (float64, bool) {
|
||
if len(north) == 0 || len(north) != len(south) {
|
||
return 0, false
|
||
}
|
||
separation, found := 0.0, false
|
||
nearestDelta := math.Inf(1)
|
||
for index := range north {
|
||
if !north[index].Time.Equal(south[index].Time) {
|
||
continue
|
||
}
|
||
value := occultationPathDistanceKM(north[index], south[index])
|
||
if !finite(value) || value < 0 {
|
||
continue
|
||
}
|
||
north[index].LimitSeparationKM = value
|
||
south[index].LimitSeparationKM = value
|
||
if delta := math.Abs(centerTimeTT(north[index].Time) - greatestTT); delta < nearestDelta {
|
||
nearestDelta, separation, found = delta, value, true
|
||
}
|
||
}
|
||
return separation, found
|
||
}
|
||
|
||
func occultationContactBandContours(
|
||
northern, southern []OccultationPathPoint,
|
||
) [][]OccultationPathPoint {
|
||
contours := make([][]OccultationPathPoint, 0, 4)
|
||
for _, source := range [][]OccultationPathPoint{northern, southern} {
|
||
for _, sampleRange := range occultationContinuousBoundaryRanges(source) {
|
||
if sampleRange.end-sampleRange.start < 2 {
|
||
continue
|
||
}
|
||
contour := append([]OccultationPathPoint(nil), source[sampleRange.start:sampleRange.end]...)
|
||
contours = append(contours, contour)
|
||
}
|
||
}
|
||
return contours
|
||
}
|
||
|
||
type occultationPathSampleRange struct {
|
||
start int
|
||
end int
|
||
}
|
||
|
||
func occultationContinuousBoundaryRanges(points []OccultationPathPoint) []occultationPathSampleRange {
|
||
if len(points) == 0 {
|
||
return nil
|
||
}
|
||
ranges := make([]occultationPathSampleRange, 0, 4)
|
||
start := 0
|
||
for index := 1; index < len(points); index++ {
|
||
if !occultationPathBoundaryBranchChanged(points[index-1], points[index]) {
|
||
continue
|
||
}
|
||
ranges = append(ranges, occultationPathSampleRange{start: start, end: index})
|
||
start = index
|
||
}
|
||
return append(ranges, occultationPathSampleRange{start: start, end: len(points)})
|
||
}
|
||
|
||
func occultationPathBoundaryBranchChanged(first, second OccultationPathPoint) bool {
|
||
distance := occultationPathDistanceKM(first, second)
|
||
if distance <= occultationPathBoundaryBranchJumpKM {
|
||
return false
|
||
}
|
||
duration := math.Abs(second.Time.Sub(first.Time).Seconds())
|
||
return duration == 0 || distance/duration > occultationPathBoundaryBranchSpeedKMPerSecond
|
||
}
|
||
|
||
func occultationContactBandContoursWithAdditionalTimes(
|
||
start, end OccultationPathPoint,
|
||
startTT, endTT, greatestTT float64,
|
||
frameAt occultationPathFrameFunc,
|
||
options OccultationPathOptions,
|
||
location *time.Location,
|
||
additionalTimes []float64,
|
||
) [][]OccultationPathPoint {
|
||
stepDays := occultationPathContourStepDays(options)
|
||
northern, southern := occultationPathBoundaryContourSamplesForFrame(
|
||
startTT, endTT, greatestTT, frameAt, stepDays, location, additionalTimes,
|
||
)
|
||
return occultationContactBandContours(
|
||
occultationPathWithEndpoints(start, end, northern),
|
||
occultationPathWithEndpoints(start, end, southern),
|
||
)
|
||
}
|
||
|
||
func occultationPathContourStepDays(options OccultationPathOptions) float64 {
|
||
stepDays := float64(options.Step) / float64(24*time.Hour)
|
||
if stepDays <= 0 {
|
||
stepDays = occultationPathDefaultStepDays
|
||
}
|
||
if options.DisableRiseSet {
|
||
return stepDays
|
||
}
|
||
riseSetStep := options.RiseSetStep
|
||
if riseSetStep <= 0 {
|
||
riseSetStep = 5 * time.Minute
|
||
}
|
||
riseSetStepDays := float64(riseSetStep) / float64(24*time.Hour)
|
||
if riseSetStepDays > 0 && riseSetStepDays < stepDays {
|
||
stepDays = riseSetStepDays
|
||
}
|
||
return stepDays
|
||
}
|
||
|
||
func starOccultationPathWindow(seedTT, startTT, endTT float64, star StarCoordinate, center bool) (float64, float64, bool) {
|
||
return starOccultationPathWindowWithFrame(seedTT, startTT, endTT, func(tt float64) (occultationPathFrame, bool) {
|
||
return starOccultationPathFrameAt(tt, star)
|
||
}, center)
|
||
}
|
||
|
||
func starOccultationPathWindowWithFrame(
|
||
seedTT, startTT, endTT float64,
|
||
frameAt occultationPathFrameFunc,
|
||
center bool,
|
||
) (float64, float64, bool) {
|
||
return occultationPathWindowWithCandidateFrames(
|
||
seedTT, startTT, endTT, frameAt, frameAt, center,
|
||
starOccultationPathHasBoundary,
|
||
)
|
||
}
|
||
|
||
func starOccultationPathWindowWithCandidateFrames(
|
||
seedTT, startTT, endTT float64,
|
||
candidateFrameAt, exactFrameAt occultationPathFrameFunc,
|
||
center bool,
|
||
) (float64, float64, bool) {
|
||
return occultationPathWindowWithCandidateFrames(
|
||
seedTT, startTT, endTT, candidateFrameAt, exactFrameAt, center,
|
||
starOccultationPathHasBoundary,
|
||
)
|
||
}
|
||
|
||
func occultationPathWindowWithCandidateFrames(
|
||
seedTT, startTT, endTT float64,
|
||
candidateFrameAt, exactFrameAt occultationPathFrameFunc,
|
||
center bool,
|
||
hasBoundary func(occultationPathFrame) bool,
|
||
) (float64, float64, bool) {
|
||
predicate := func(frameAt occultationPathFrameFunc, tt float64) bool {
|
||
frame, ok := frameAt(tt)
|
||
if !ok {
|
||
return false
|
||
}
|
||
if center {
|
||
_, _, ok = occultationEarthLineIntersection(frame.moon, frame.axis)
|
||
return ok
|
||
}
|
||
return hasBoundary(frame)
|
||
}
|
||
engine := occultationMovingDiskEngine()
|
||
return engine.window(
|
||
seedTT, startTT, endTT,
|
||
func(tt float64) bool { return predicate(candidateFrameAt, tt) },
|
||
func(tt float64) bool { return predicate(exactFrameAt, tt) },
|
||
)
|
||
}
|
||
|
||
func starOccultationPathGreatest(seedTT, startTT, endTT float64, star StarCoordinate) float64 {
|
||
return starOccultationPathGreatestWithFrame(seedTT, startTT, endTT, func(tt float64) (occultationPathFrame, bool) {
|
||
return starOccultationPathFrameAt(tt, star)
|
||
})
|
||
}
|
||
|
||
func starOccultationPathGreatestWithFrame(
|
||
seedTT, startTT, endTT float64,
|
||
frameAt occultationPathFrameFunc,
|
||
) float64 {
|
||
return starOccultationPathGreatestWithIterations(seedTT, startTT, endTT, frameAt, 56)
|
||
}
|
||
|
||
func starOccultationPathGreatestWithIterations(
|
||
seedTT, startTT, endTT float64,
|
||
frameAt occultationPathFrameFunc,
|
||
iterations int,
|
||
) float64 {
|
||
return occultationMovingDiskEngine().greatest(
|
||
seedTT, startTT, endTT,
|
||
func(tt float64) (float64, bool) {
|
||
frame, ok := frameAt(tt)
|
||
if !ok {
|
||
return 0, false
|
||
}
|
||
return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY()), true
|
||
}, iterations,
|
||
)
|
||
}
|
||
|
||
func starOccultationPathImpactWithFrame(tt float64, frameAt occultationPathFrameFunc) float64 {
|
||
frame, ok := frameAt(tt)
|
||
if !ok {
|
||
return math.Inf(1)
|
||
}
|
||
return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY())
|
||
}
|
||
|
||
func starOccultationPathSamplesWithFrame(
|
||
outerStartTT, outerEndTT float64,
|
||
centerStartTT, centerEndTT float64,
|
||
hasCenter bool,
|
||
greatestTT float64,
|
||
frameAt occultationPathFrameFunc,
|
||
options OccultationPathOptions,
|
||
location *time.Location,
|
||
) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) {
|
||
var points []OccultationPathPoint
|
||
if hasCenter {
|
||
var err error
|
||
points, err = starOccultationPathCenterSamplesWithFrame(centerStartTT, centerEndTT, greatestTT, frameAt, options, location)
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
}
|
||
|
||
northern, southern := occultationPathBoundarySamplesForFrame(
|
||
outerStartTT, outerEndTT, greatestTT, frameAt, occultationPathContourStepDays(options), location,
|
||
)
|
||
return points, northern, southern, nil
|
||
}
|
||
|
||
func starOccultationPathCenterSamplesWithFrame(
|
||
startTT, endTT, greatestTT float64,
|
||
frameAt occultationPathFrameFunc,
|
||
options OccultationPathOptions,
|
||
location *time.Location,
|
||
) ([]OccultationPathPoint, error) {
|
||
stepDays := float64(options.Step) / float64(24*time.Hour)
|
||
times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays)
|
||
points := make([]OccultationPathPoint, 0, len(times))
|
||
for _, tt := range times {
|
||
point, ok := starOccultationPathCenterPointWithFrame(tt, frameAt, location)
|
||
if ok && (len(points) == 0 || point.Time.After(points[len(points)-1].Time)) {
|
||
points = append(points, point)
|
||
}
|
||
}
|
||
if options.TargetSpacingKM > 0 {
|
||
return refineOccultationPathSpacingWithFrame(points, frameAt, options.TargetSpacingKM, location)
|
||
}
|
||
return points, nil
|
||
}
|
||
|
||
func occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays float64) []float64 {
|
||
return occultationPathSampleTimesWithLimit(
|
||
startTT, endTT, greatestTT, stepDays, occultationPathMaxSampleCount,
|
||
)
|
||
}
|
||
|
||
func occultationPathSampleTimesWithLimit(
|
||
startTT, endTT, greatestTT, stepDays float64,
|
||
maximumCount int,
|
||
) []float64 {
|
||
if maximumCount < 3 {
|
||
maximumCount = 3
|
||
}
|
||
engine := occultationMovingDiskEngine()
|
||
engine.maxSampleCount = maximumCount
|
||
times, _ := engine.sampleTimes(startTT, endTT, greatestTT, stepDays)
|
||
return times
|
||
}
|
||
|
||
func refineOccultationPathSpacingWithFrame(
|
||
points []OccultationPathPoint,
|
||
frameAt occultationPathFrameFunc,
|
||
targetSpacingKM float64,
|
||
location *time.Location,
|
||
) ([]OccultationPathPoint, error) {
|
||
if len(points) < 2 || targetSpacingKM <= 0 {
|
||
return points, nil
|
||
}
|
||
refined := make([]OccultationPathPoint, 0, len(points))
|
||
refined = append(refined, points[0])
|
||
widthAt := func(tt float64) (float64, bool) {
|
||
_, _, width, ok := occultationPathLimitsAndWidthForFrame(tt, frameAt)
|
||
return width, ok
|
||
}
|
||
for i := 1; i < len(points); i++ {
|
||
segmentStart := len(refined) - 1
|
||
var err error
|
||
refined, err = appendOccultationPathSegmentWithFrame(refined, points[i-1], points[i], frameAt, targetSpacingKM, location, 0)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
refineOccultationPathWidths(refined[segmentStart:], widthAt)
|
||
}
|
||
return refined, nil
|
||
}
|
||
|
||
func appendOccultationPathSegmentWithFrame(
|
||
points []OccultationPathPoint,
|
||
start, end OccultationPathPoint,
|
||
frameAt occultationPathFrameFunc,
|
||
targetSpacingKM float64,
|
||
location *time.Location,
|
||
depth int,
|
||
) ([]OccultationPathPoint, error) {
|
||
distance := occultationPathDistanceKM(start, end)
|
||
if distance <= targetSpacingKM {
|
||
if len(points) >= occultationPathMaxSampleCount {
|
||
return nil, ErrOccultationPathSamplingLimit
|
||
}
|
||
return append(points, end), nil
|
||
}
|
||
if depth >= occultationPathMaxAdaptiveDepth || len(points) >= occultationPathMaxSampleCount {
|
||
return nil, ErrOccultationPathSamplingLimit
|
||
}
|
||
startTT := centerTimeTT(start.Time)
|
||
endTT := centerTimeTT(end.Time)
|
||
midTT := (startTT + endTT) / 2
|
||
midTime := occultationTTToLocation(midTT, location)
|
||
if !midTime.After(start.Time) || !midTime.Before(end.Time) {
|
||
return append(points, end), nil
|
||
}
|
||
mid, ok := starOccultationPathCenterPointWithoutWidthWithFrame(midTT, frameAt, location)
|
||
if !ok {
|
||
return append(points, end), nil
|
||
}
|
||
mid.WidthKM = (start.WidthKM + end.WidthKM) / 2
|
||
var err error
|
||
points, err = appendOccultationPathSegmentWithFrame(points, start, mid, frameAt, targetSpacingKM, location, depth+1)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return appendOccultationPathSegmentWithFrame(points, mid, end, frameAt, targetSpacingKM, location, depth+1)
|
||
}
|
||
|
||
func uniqueOccultationPathTimes(times []float64) []float64 {
|
||
return movingDiskUniqueTimes(times)
|
||
}
|
||
|
||
type occultationPathFrame struct {
|
||
moon occultationPathVector
|
||
axis occultationPathVector
|
||
first occultationPathVector
|
||
second occultationPathVector
|
||
moonRadius float64
|
||
targetRadius float64
|
||
// boundary 挂在被事件缓存复用的 frame 上,记忆化只依赖 frame 的边界搜索;零值 frame 为 nil 时退化为直接计算。
|
||
// boundary is attached to frames reused through the event cache and memoizes the boundary
|
||
// searches that depend only on the frame; a zero frame keeps it nil and computes directly.
|
||
boundary *occultationPathBoundaryCache
|
||
}
|
||
|
||
// occultationPathBoundaryCache 记忆化只依赖 frame 的两个边界搜索:切点与可见 θ 区间。
|
||
// 两者各自要扫描 720 个网格点,而同一 frame 会在多个调用点被反复查询。
|
||
// occultationPathBoundaryCache memoizes the two frame-only boundary searches (the tangency
|
||
// and the visible theta intervals). Each scans 720 grid points and the same frame is queried
|
||
// repeatedly from several call sites.
|
||
type occultationPathBoundaryCache struct {
|
||
// boundaryOnce 用同一份 720 点判别式网格同时恢复切点与可见 θ 区间,避免两条路径各扫一遍。
|
||
// boundaryOnce recovers both the tangency and the visible theta intervals from one 720-point
|
||
// discriminant grid instead of scanning it once per path.
|
||
boundaryOnce sync.Once
|
||
tangentPoint occultationPathVector
|
||
tangentTheta float64
|
||
tangentOK bool
|
||
intervals []occultationPathThetaInterval
|
||
|
||
// intervalMu 保护按 centerTheta 复用的 θ 区间搜索;每个 frame 只会用到个位数个中心角。
|
||
// intervalMu guards the theta-interval searches reused per center angle; a frame only ever
|
||
// uses a handful of center angles.
|
||
intervalMu sync.Mutex
|
||
intervalKeys []uint64
|
||
intervalVals []occultationPathThetaIntervalResult
|
||
}
|
||
|
||
// occultationPathFrameGeometryEqual 比较两个 frame 的几何内容,忽略只用于记忆化的 boundary 指针。
|
||
// occultationPathFrameGeometryEqual compares the geometric content of two frames and ignores
|
||
// the memo-only boundary pointer.
|
||
func occultationPathFrameGeometryEqual(first, second occultationPathFrame) bool {
|
||
return first.moon == second.moon &&
|
||
first.axis == second.axis &&
|
||
first.first == second.first &&
|
||
first.second == second.second &&
|
||
first.moonRadius == second.moonRadius &&
|
||
first.targetRadius == second.targetRadius
|
||
}
|
||
|
||
// occultationPathThetaIntervalResult 是 occultationPathBoundaryThetaInterval 的缓存结果。
|
||
// occultationPathThetaIntervalResult is the cached result of occultationPathBoundaryThetaInterval.
|
||
type occultationPathThetaIntervalResult struct {
|
||
left, right float64
|
||
ok bool
|
||
}
|
||
|
||
type starOccultationEphemerisState struct {
|
||
moonRA, moonDec float64
|
||
moonDistanceKM float64
|
||
starRA, starDec float64
|
||
starDistanceKM float64
|
||
valid bool
|
||
}
|
||
|
||
type starOccultationEventCache struct {
|
||
star StarCoordinate
|
||
states map[uint64]starOccultationEphemerisState
|
||
frames map[uint64]planetOccultationFrameCacheEntry
|
||
riseSetCache *occultationRiseSetEvaluationCache
|
||
local *starOccultationLocalEphemeris
|
||
}
|
||
|
||
func newStarOccultationEventCache(star StarCoordinate) *starOccultationEventCache {
|
||
cache := &starOccultationEventCache{
|
||
star: star,
|
||
states: make(map[uint64]starOccultationEphemerisState),
|
||
frames: make(map[uint64]planetOccultationFrameCacheEntry),
|
||
}
|
||
cache.riseSetCache = newOccultationRiseSetEvaluationCacheWithCandidate(
|
||
cache.riseSetContextAt,
|
||
cache.candidateRiseSetContextAt,
|
||
)
|
||
return cache
|
||
}
|
||
|
||
func (cache *starOccultationEventCache) stateAt(tt float64) starOccultationEphemerisState {
|
||
key := math.Float64bits(tt)
|
||
if state, ok := cache.states[key]; ok {
|
||
return state
|
||
}
|
||
if len(cache.states) >= planetOccultationEventCacheMaximumEntries {
|
||
for cachedKey := range cache.states {
|
||
delete(cache.states, cachedKey)
|
||
}
|
||
for cachedKey := range cache.frames {
|
||
delete(cache.frames, cachedKey)
|
||
}
|
||
}
|
||
var state starOccultationEphemerisState
|
||
interpolated := false
|
||
if cache.local != nil && cache.local.dense {
|
||
state, interpolated = cache.local.stateAt(tt)
|
||
}
|
||
if !interpolated {
|
||
state = starOccultationEphemerisStateAt(tt, cache.star)
|
||
}
|
||
cache.states[key] = state
|
||
return state
|
||
}
|
||
|
||
func (cache *starOccultationEventCache) prepareLocalEphemeris(center float64) {
|
||
if cache.local == nil {
|
||
cache.local = newStarOccultationLocalEphemeris(center, cache.star)
|
||
}
|
||
}
|
||
|
||
func (cache *starOccultationEventCache) candidateFrameAt(tt float64) (occultationPathFrame, bool) {
|
||
if cache.local != nil {
|
||
if state, ok := cache.local.stateAt(tt); ok {
|
||
return starOccultationPathFrameFromState(state)
|
||
}
|
||
}
|
||
return cache.frameAt(tt)
|
||
}
|
||
|
||
func (cache *starOccultationEventCache) frameAt(tt float64) (occultationPathFrame, bool) {
|
||
key := math.Float64bits(tt)
|
||
if entry, ok := cache.frames[key]; ok {
|
||
return entry.frame, entry.ok
|
||
}
|
||
frame, ok := starOccultationPathFrameFromState(cache.stateAt(tt))
|
||
if ok {
|
||
frame.boundary = &occultationPathBoundaryCache{}
|
||
}
|
||
cache.frames[key] = planetOccultationFrameCacheEntry{frame: frame, ok: ok}
|
||
return frame, ok
|
||
}
|
||
|
||
func (cache *starOccultationEventCache) riseSetContextAt(tt float64) occultationRiseSetContext {
|
||
state := cache.stateAt(tt)
|
||
return newOccultationRiseSetContext(
|
||
tt, state.moonRA, state.moonDec, state.moonDistanceKM,
|
||
state.starRA, state.starDec, state.starDistanceKM, 0,
|
||
)
|
||
}
|
||
|
||
func (cache *starOccultationEventCache) candidateRiseSetContextAt(tt float64) occultationRiseSetContext {
|
||
if cache.local != nil {
|
||
moonXYZ, targetXYZ, ok := cache.local.vectorsAt(tt)
|
||
if ok {
|
||
return newOccultationRiseSetContextFromVectors(
|
||
tt,
|
||
moonXYZ,
|
||
targetXYZ,
|
||
cache.local.starDistanceKM() > 0,
|
||
0,
|
||
)
|
||
}
|
||
}
|
||
return cache.riseSetContextAt(tt)
|
||
}
|
||
|
||
type occultationPathEndpoint struct {
|
||
point OccultationPathPoint
|
||
valid bool
|
||
}
|
||
|
||
func (f occultationPathFrame) moonProjectionX() float64 { return occultationPathDot(f.moon, f.first) }
|
||
func (f occultationPathFrame) moonProjectionY() float64 { return occultationPathDot(f.moon, f.second) }
|
||
|
||
func starOccultationPathFrameAt(tt float64, star StarCoordinate) (occultationPathFrame, bool) {
|
||
return starOccultationPathFrameFromState(starOccultationEphemerisStateAt(tt, star))
|
||
}
|
||
|
||
func starOccultationEphemerisStateAt(tt float64, star StarCoordinate) starOccultationEphemerisState {
|
||
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
|
||
moonDistanceKM := HMoonAwayN(tt, -1)
|
||
// 方向与距离必须来自同一次三维推进,否则视差修正会退回历元距离。
|
||
starRA, starDec, starDistanceAU := starApparentRaDecDistanceGeocentric(tt, star)
|
||
starDistanceKM := starDistanceAU * occultationPathAstronomicalUnitKM
|
||
return starOccultationEphemerisState{
|
||
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistanceKM,
|
||
starRA: starRA, starDec: starDec, starDistanceKM: starDistanceKM,
|
||
valid: finite(moonRA) && finite(moonDec) && finite(moonDistanceKM) && moonDistanceKM > 0 &&
|
||
finite(starRA) && finite(starDec) && finite(starDistanceKM) && starDistanceKM >= 0,
|
||
}
|
||
}
|
||
|
||
func starOccultationPathFrameFromState(state starOccultationEphemerisState) (occultationPathFrame, bool) {
|
||
if !state.valid {
|
||
return occultationPathFrame{}, false
|
||
}
|
||
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
|
||
starDirection := occultationPathRaDecVector(state.starRA, state.starDec, 1)
|
||
axis := occultationPathScale(starDirection, -1)
|
||
if state.starDistanceKM > 0 {
|
||
target := occultationPathRaDecVector(state.starRA, state.starDec, state.starDistanceKM)
|
||
axis = occultationPathScale(occultationPathSub(target, moon), -1)
|
||
}
|
||
axis = occultationPathUnit(axis)
|
||
north := occultationPathVector{z: 1}
|
||
first := occultationPathCross(north, axis)
|
||
if occultationPathNorm(first) < 1e-12 {
|
||
first = occultationPathCross(occultationPathVector{x: 1}, axis)
|
||
}
|
||
first = occultationPathUnit(first)
|
||
second := occultationPathUnit(occultationPathCross(axis, first))
|
||
return occultationPathFrame{
|
||
moon: moon,
|
||
axis: axis,
|
||
first: first,
|
||
second: second,
|
||
moonRadius: math.Asin(moonEquatorialRadiusKM / state.moonDistanceKM),
|
||
}, true
|
||
}
|
||
|
||
func starOccultationPathHasBoundary(frame occultationPathFrame) bool {
|
||
_, _, ok := occultationPathBoundaryTangent(frame)
|
||
return ok
|
||
}
|
||
|
||
func starOccultationPathBoundaryEndpointWithFrame(
|
||
tt float64,
|
||
frameAt occultationPathFrameFunc,
|
||
location *time.Location,
|
||
direction int,
|
||
) occultationPathEndpoint {
|
||
if _, ok := frameAt(tt); !ok {
|
||
return occultationPathEndpoint{}
|
||
}
|
||
for offset := 0; offset <= 3; offset++ {
|
||
candidateTT := tt + float64(direction)*float64(offset)*0.5/86400.0
|
||
candidateFrame, candidateOK := frameAt(candidateTT)
|
||
if !candidateOK {
|
||
continue
|
||
}
|
||
vector, _, valid := occultationPathBoundaryTangent(candidateFrame)
|
||
if valid {
|
||
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true}
|
||
}
|
||
}
|
||
return occultationPathEndpoint{}
|
||
}
|
||
|
||
func starOccultationPathCenterPoint(tt float64, star StarCoordinate, location *time.Location) (OccultationPathPoint, bool) {
|
||
return starOccultationPathCenterPointWithFrame(tt, func(candidateTT float64) (occultationPathFrame, bool) {
|
||
return starOccultationPathFrameAt(candidateTT, star)
|
||
}, location)
|
||
}
|
||
|
||
func starOccultationPathCenterPointWithFrame(
|
||
tt float64,
|
||
frameAt occultationPathFrameFunc,
|
||
location *time.Location,
|
||
) (OccultationPathPoint, bool) {
|
||
frame, ok := frameAt(tt)
|
||
if !ok {
|
||
return OccultationPathPoint{}, false
|
||
}
|
||
point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis)
|
||
if !ok {
|
||
return OccultationPathPoint{}, false
|
||
}
|
||
width := 0.0
|
||
if _, _, tangentWidth, limitsOK := occultationPathLimitsAndWidthForFrame(tt, frameAt); limitsOK {
|
||
width = tangentWidth
|
||
}
|
||
return occultationPathPointFromVectorWithMoon(tt, point, width, frame.moon, location), true
|
||
}
|
||
|
||
func starOccultationPathCenterPointWithoutWidthWithFrame(
|
||
tt float64,
|
||
frameAt occultationPathFrameFunc,
|
||
location *time.Location,
|
||
) (OccultationPathPoint, bool) {
|
||
frame, ok := frameAt(tt)
|
||
if !ok {
|
||
return OccultationPathPoint{}, false
|
||
}
|
||
point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis)
|
||
if !ok {
|
||
return OccultationPathPoint{}, false
|
||
}
|
||
return occultationPathPointFromVectorWithMoon(tt, point, 0, frame.moon, location), true
|
||
}
|
||
|
||
func occultationPathScannedLimitsAtFrame(tt float64, frame occultationPathFrame) (occultationPathVector, occultationPathVector, bool) {
|
||
var northern, southern occultationPathVector
|
||
northLatitude := math.Inf(-1)
|
||
southLatitude := math.Inf(1)
|
||
consider := func(vector occultationPathVector) {
|
||
latitude := occultationPathGeodeticLatitude(vector)
|
||
if latitude > northLatitude {
|
||
northLatitude = latitude
|
||
northern = vector
|
||
}
|
||
if latitude < southLatitude {
|
||
southLatitude = latitude
|
||
southern = vector
|
||
}
|
||
}
|
||
if tangentPoint, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame); tangentOK {
|
||
consider(tangentPoint)
|
||
if leftTheta, rightTheta, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta); intervalOK {
|
||
const intervalSamples = 128
|
||
for i := 0; i <= intervalSamples; i++ {
|
||
theta := leftTheta + (rightTheta-leftTheta)*float64(i)/intervalSamples
|
||
if vector, _, ok := occultationPathBoundaryVector(frame, theta); ok {
|
||
consider(vector)
|
||
}
|
||
}
|
||
// 同 occultationPathFiniteCrossTrackExtrema:等差末样本与 rightTheta 差 1 ULP,
|
||
// 掠射时端点会被判"无地面交点"而丢掉极值,端点原值必须补采一次。
|
||
if vector, _, ok := occultationPathBoundaryVector(frame, rightTheta); ok {
|
||
consider(vector)
|
||
}
|
||
}
|
||
}
|
||
for i := 0; i < occultationPathBoundaryScanPoints; i++ {
|
||
vector, _, ok := occultationPathBoundaryVector(frame, 2*math.Pi*float64(i)/float64(occultationPathBoundaryScanPoints))
|
||
if !ok {
|
||
continue
|
||
}
|
||
consider(vector)
|
||
}
|
||
return northern, southern, finite(northLatitude) && finite(southLatitude)
|
||
}
|
||
|
||
func occultationPathBoundaryThetaInterval(frame occultationPathFrame, centerTheta float64) (float64, float64, bool) {
|
||
if cached, hit := occultationPathCachedThetaInterval(frame, centerTheta); hit {
|
||
return cached.left, cached.right, cached.ok
|
||
}
|
||
if !occultationPathBoundaryLineIntersects(frame, centerTheta) {
|
||
return 0, 0, false
|
||
}
|
||
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
|
||
findEdge := func(direction float64) (float64, bool) {
|
||
inside := centerTheta
|
||
for i := 1; i <= occultationPathBoundaryScanPoints; i++ {
|
||
outside := centerTheta + direction*step*float64(i)
|
||
if occultationPathBoundaryLineIntersects(frame, outside) {
|
||
inside = outside
|
||
continue
|
||
}
|
||
for iteration := 0; iteration < 56; iteration++ {
|
||
mid := (inside + outside) / 2
|
||
if occultationPathBoundaryLineIntersects(frame, mid) {
|
||
inside = mid
|
||
} else {
|
||
outside = mid
|
||
}
|
||
}
|
||
return inside, true
|
||
}
|
||
return 0, false
|
||
}
|
||
left, leftOK := findEdge(-1)
|
||
right, rightOK := findEdge(1)
|
||
result := occultationPathThetaIntervalResult{left: left, right: right, ok: leftOK && rightOK && right > left}
|
||
occultationPathStoreThetaInterval(frame, centerTheta, result)
|
||
return result.left, result.right, result.ok
|
||
}
|
||
|
||
// occultationPathCachedThetaInterval 查询 frame 上按 centerTheta 缓存的 θ 区间。
|
||
// occultationPathCachedThetaInterval looks up a theta interval cached on the frame.
|
||
func occultationPathCachedThetaInterval(
|
||
frame occultationPathFrame,
|
||
centerTheta float64,
|
||
) (occultationPathThetaIntervalResult, bool) {
|
||
if frame.boundary == nil {
|
||
return occultationPathThetaIntervalResult{}, false
|
||
}
|
||
key := math.Float64bits(centerTheta)
|
||
frame.boundary.intervalMu.Lock()
|
||
defer frame.boundary.intervalMu.Unlock()
|
||
for index, cached := range frame.boundary.intervalKeys {
|
||
if cached == key {
|
||
return frame.boundary.intervalVals[index], true
|
||
}
|
||
}
|
||
return occultationPathThetaIntervalResult{}, false
|
||
}
|
||
|
||
// occultationPathStoreThetaInterval 记住 frame 上某个 centerTheta 的 θ 区间结果。
|
||
// occultationPathStoreThetaInterval remembers one theta interval computed on a frame.
|
||
func occultationPathStoreThetaInterval(
|
||
frame occultationPathFrame,
|
||
centerTheta float64,
|
||
result occultationPathThetaIntervalResult,
|
||
) {
|
||
if frame.boundary == nil {
|
||
return
|
||
}
|
||
key := math.Float64bits(centerTheta)
|
||
frame.boundary.intervalMu.Lock()
|
||
defer frame.boundary.intervalMu.Unlock()
|
||
if len(frame.boundary.intervalKeys) >= occultationPathThetaIntervalCacheMaximumEntries {
|
||
return
|
||
}
|
||
frame.boundary.intervalKeys = append(frame.boundary.intervalKeys, key)
|
||
frame.boundary.intervalVals = append(frame.boundary.intervalVals, result)
|
||
}
|
||
|
||
func occultationPathBoundaryLineIntersects(frame occultationPathFrame, theta float64) bool {
|
||
discriminant, b, _, ok := occultationPathBoundaryLine(frame, theta)
|
||
return ok && b < 0 && discriminant >= 0
|
||
}
|
||
|
||
func occultationPathBoundaryWidth(north, south occultationPathVector, ok bool) float64 {
|
||
if !ok {
|
||
return 0
|
||
}
|
||
return occultationPathNorm(occultationPathSub(north, south))
|
||
}
|
||
|
||
func occultationPathBoundaryVector(frame occultationPathFrame, theta float64) (occultationPathVector, float64, bool) {
|
||
origin, direction, ok := occultationPathBoundaryRay(frame, theta)
|
||
if !ok {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
return occultationEarthLineIntersection(origin, direction)
|
||
}
|
||
|
||
// occultationPathBoundaryRay 返回月缘圆柱或两球公切锥的一个母线 /
|
||
// occultationPathBoundaryRay returns one generator of the lunar-limb cylinder or a two-sphere common-tangent cone.
|
||
// targetRadius 带符号:正值表示异侧外切,负值表示同侧内切 /
|
||
// targetRadius is signed: positive for opposite-side outer tangency and negative for same-side inner tangency.
|
||
func occultationPathBoundaryRay(frame occultationPathFrame, theta float64) (occultationPathVector, occultationPathVector, bool) {
|
||
moonDistance := occultationPathNorm(frame.moon)
|
||
if moonDistance <= 0 || !finite(moonDistance) || !finite(frame.targetRadius) {
|
||
return occultationPathVector{}, occultationPathVector{}, false
|
||
}
|
||
sineTheta, cosineTheta := math.Sincos(theta)
|
||
radial := occultationPathAdd(
|
||
occultationPathScale(frame.first, cosineTheta),
|
||
occultationPathScale(frame.second, sineTheta),
|
||
)
|
||
sine, cosine := math.Sincos(frame.targetRadius)
|
||
normal := occultationPathAdd(
|
||
occultationPathScale(radial, cosine),
|
||
occultationPathScale(frame.axis, -sine),
|
||
)
|
||
origin := occultationPathAdd(
|
||
frame.moon,
|
||
occultationPathScale(normal, moonDistance*math.Sin(frame.moonRadius)),
|
||
)
|
||
direction := occultationPathAdd(
|
||
occultationPathScale(frame.axis, cosine),
|
||
occultationPathScale(radial, sine),
|
||
)
|
||
return origin, occultationPathUnit(direction), true
|
||
}
|
||
|
||
// occultationPathBoundaryTangent 求边界锥与地球椭球的连续切点 /
|
||
// occultationPathBoundaryTangent finds the continuous tangency of a boundary cone with the Earth ellipsoid.
|
||
// 采样网格提供搜索盆地,再对线判别式做局部极大化以恢复网格点之间的切点 /
|
||
// A sample grid supplies a basin, while local maximization of the line discriminant recovers tangencies between grid points.
|
||
func occultationPathBoundaryTangent(frame occultationPathFrame) (occultationPathVector, float64, bool) {
|
||
if frame.boundary != nil {
|
||
frame.boundary.boundaryOnce.Do(func() { occultationPathBoundaryPrecompute(frame) })
|
||
return frame.boundary.tangentPoint, frame.boundary.tangentTheta, frame.boundary.tangentOK
|
||
}
|
||
return occultationPathBoundaryTangentUncached(frame)
|
||
}
|
||
|
||
// occultationPathBoundaryPrecompute 用一份网格同时算好切点与可见 θ 区间。
|
||
// occultationPathBoundaryPrecompute derives both the tangency and the visible theta intervals
|
||
// from one discriminant grid.
|
||
func occultationPathBoundaryPrecompute(frame occultationPathFrame) {
|
||
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
|
||
discriminants := make([]float64, occultationPathBoundaryScanPoints)
|
||
occultationPathBoundaryFillGrid(frame, discriminants)
|
||
frame.boundary.tangentPoint, frame.boundary.tangentTheta, frame.boundary.tangentOK =
|
||
occultationPathBoundaryTangentFromGrid(frame, step, discriminants)
|
||
frame.boundary.intervals = occultationPathBoundaryThetaIntervalsFromGrid(frame, step, discriminants)
|
||
}
|
||
|
||
// occultationPathBoundaryTangentUncached 是切点搜索本体;调用方通过 frame 级缓存复用结果。
|
||
// occultationPathBoundaryTangentUncached is the tangency search itself; callers reuse it through the frame cache.
|
||
func occultationPathBoundaryTangentUncached(frame occultationPathFrame) (occultationPathVector, float64, bool) {
|
||
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
|
||
discriminants := make([]float64, occultationPathBoundaryScanPoints)
|
||
occultationPathBoundaryFillGrid(frame, discriminants)
|
||
return occultationPathBoundaryTangentFromGrid(frame, step, discriminants)
|
||
}
|
||
|
||
// occultationPathBoundaryTangentFromGrid 在已算好的网格上恢复切点。
|
||
// occultationPathBoundaryTangentFromGrid recovers the tangency from a prepared grid.
|
||
func occultationPathBoundaryTangentFromGrid(
|
||
frame occultationPathFrame,
|
||
step float64,
|
||
discriminants []float64,
|
||
) (occultationPathVector, float64, bool) {
|
||
bestTheta := 0.0
|
||
bestDiscriminant := math.Inf(-1)
|
||
for i, discriminant := range discriminants {
|
||
if discriminant > bestDiscriminant {
|
||
bestDiscriminant = discriminant
|
||
bestTheta = step * float64(i)
|
||
}
|
||
}
|
||
if !finite(bestDiscriminant) {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
|
||
left := bestTheta - step
|
||
right := bestTheta + step
|
||
const goldenRatio = 0.6180339887498949
|
||
x1 := right - goldenRatio*(right-left)
|
||
x2 := left + goldenRatio*(right-left)
|
||
f1, _, _, _ := occultationPathBoundaryLine(frame, x1)
|
||
f2, _, _, _ := occultationPathBoundaryLine(frame, x2)
|
||
for i := 0; i < 40; i++ {
|
||
if f1 < f2 {
|
||
left = x1
|
||
x1, f1 = x2, f2
|
||
x2 = left + goldenRatio*(right-left)
|
||
f2, _, _, _ = occultationPathBoundaryLine(frame, x2)
|
||
} else {
|
||
right = x2
|
||
x2, f2 = x1, f1
|
||
x1 = right - goldenRatio*(right-left)
|
||
f1, _, _, _ = occultationPathBoundaryLine(frame, x1)
|
||
}
|
||
}
|
||
theta := (left + right) / 2
|
||
discriminant, b, scale, ok := occultationPathBoundaryLine(frame, theta)
|
||
if !ok {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
tolerance := 1e-12 * math.Max(scale, 1)
|
||
if discriminant < -tolerance || b >= 0 {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
vector, _, valid := occultationPathBoundaryIntersection(frame, theta, tolerance)
|
||
return vector, theta, valid
|
||
}
|
||
|
||
func occultationPathBoundaryLine(frame occultationPathFrame, theta float64) (discriminant, b, scale float64, ok bool) {
|
||
origin, direction, rayOK := occultationPathBoundaryRay(frame, theta)
|
||
if !rayOK {
|
||
return 0, 0, 0, false
|
||
}
|
||
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
|
||
a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared
|
||
b = origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared
|
||
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
|
||
discriminant = b*b - a*c
|
||
scale = math.Max(math.Abs(b*b), math.Abs(a*c))
|
||
return discriminant, b, scale, a > 0 && finite(discriminant)
|
||
}
|
||
|
||
func occultationPathBoundaryIntersection(frame occultationPathFrame, theta, tolerance float64) (occultationPathVector, float64, bool) {
|
||
origin, direction, ok := occultationPathBoundaryRay(frame, theta)
|
||
if !ok {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
return occultationEarthLineIntersectionWithTolerance(origin, direction, tolerance)
|
||
}
|
||
|
||
func occultationPathPointFromVector(tt float64, vector occultationPathVector, width float64, location *time.Location) OccultationPathPoint {
|
||
lon, lat := occultationPathGeodetic(tt, vector)
|
||
moonRA, moonDec := moonTopocentricApparentRaDec(tt, Observer{Longitude: lon, Latitude: lat}, -1)
|
||
return OccultationPathPoint{
|
||
Time: occultationTTToLocation(tt, location),
|
||
Longitude: lon,
|
||
Latitude: lat,
|
||
MoonAltitude: occultationAltitude(tt, Observer{Longitude: lon, Latitude: lat}, moonRA, moonDec),
|
||
WidthKM: width,
|
||
}
|
||
}
|
||
|
||
func occultationPathPointFromVectorWithMoon(
|
||
tt float64,
|
||
vector occultationPathVector,
|
||
width float64,
|
||
moon occultationPathVector,
|
||
location *time.Location,
|
||
) OccultationPathPoint {
|
||
return occultationPathPointFromVectorWithMoonSidereal(
|
||
tt, vector, width, moon, ApparentSiderealTime(TT2UT1(tt))*15, location,
|
||
)
|
||
}
|
||
|
||
func occultationPathPointFromVectorWithMoonSidereal(
|
||
tt float64,
|
||
vector occultationPathVector,
|
||
width float64,
|
||
moon occultationPathVector,
|
||
siderealDegrees float64,
|
||
location *time.Location,
|
||
) OccultationPathPoint {
|
||
lon, lat := occultationPathGeodeticWithSidereal(vector, siderealDegrees)
|
||
moonDistance := occultationPathNorm(moon)
|
||
moonRA := normalizeRA(math.Atan2(moon.y, moon.x) * 180 / math.Pi)
|
||
moonDec := math.Asin(math.Max(-1, math.Min(1, moon.z/moonDistance))) * 180 / math.Pi
|
||
moonRA, moonDec = topocentricRaDecWithSidereal(
|
||
moonRA, moonDec, lat, lon, siderealDegrees,
|
||
moonDistance/occultationPathAstronomicalUnitKM, 0,
|
||
)
|
||
return OccultationPathPoint{
|
||
Time: occultationTTToLocation(tt, location),
|
||
Longitude: lon,
|
||
Latitude: lat,
|
||
MoonAltitude: occultationAltitudeWithSidereal(
|
||
siderealDegrees, Observer{Longitude: lon, Latitude: lat}, normalizeRA(moonRA), moonDec,
|
||
),
|
||
WidthKM: width,
|
||
}
|
||
}
|
||
|
||
func occultationPathDeduplicateBoundaryPoints(
|
||
first, second []OccultationPathPoint,
|
||
) ([]OccultationPathPoint, []OccultationPathPoint) {
|
||
if len(first) != len(second) || len(first) == 0 {
|
||
return first, second
|
||
}
|
||
uniqueFirst := make([]OccultationPathPoint, 0, len(first))
|
||
uniqueSecond := make([]OccultationPathPoint, 0, len(second))
|
||
for index := range first {
|
||
if len(uniqueFirst) > 0 && !first[index].Time.After(uniqueFirst[len(uniqueFirst)-1].Time) {
|
||
continue
|
||
}
|
||
if len(uniqueFirst) > 0 && first[index].Time.Sub(uniqueFirst[len(uniqueFirst)-1].Time) < time.Second &&
|
||
occultationPathDistanceKM(first[index], uniqueFirst[len(uniqueFirst)-1]) < occultationPathBoundaryMergeKM &&
|
||
occultationPathDistanceKM(second[index], uniqueSecond[len(uniqueSecond)-1]) < occultationPathBoundaryMergeKM {
|
||
uniqueFirst[len(uniqueFirst)-1] = first[index]
|
||
uniqueSecond[len(uniqueSecond)-1] = second[index]
|
||
continue
|
||
}
|
||
uniqueFirst = append(uniqueFirst, first[index])
|
||
uniqueSecond = append(uniqueSecond, second[index])
|
||
}
|
||
return uniqueFirst, uniqueSecond
|
||
}
|
||
|
||
func centerTimeTT(value time.Time) float64 { return occultationTimeToTT(value) }
|
||
|
||
type occultationPathWidthFunc func(float64) (float64, bool)
|
||
|
||
func refineOccultationPathWidths(points []OccultationPathPoint, widthAt occultationPathWidthFunc) {
|
||
if len(points) < 3 {
|
||
return
|
||
}
|
||
cache := make(map[int]bool)
|
||
var refine func(int, int, int)
|
||
refine = func(left, right, depth int) {
|
||
if right-left <= 1 || depth >= occultationPathMaxAdaptiveDepth {
|
||
return
|
||
}
|
||
if right-left <= 4 {
|
||
for index := left + 1; index < right; index++ {
|
||
setOccultationPathExactWidth(points, index, widthAt, cache)
|
||
}
|
||
return
|
||
}
|
||
|
||
indices := uniqueOccultationPathWidthIndices(left, right)
|
||
withinTolerance := true
|
||
leftTime := points[left].Time
|
||
duration := points[right].Time.Sub(leftTime).Seconds()
|
||
for _, index := range indices {
|
||
linear := (points[left].WidthKM + points[right].WidthKM) / 2
|
||
if duration != 0 {
|
||
fraction := points[index].Time.Sub(leftTime).Seconds() / duration
|
||
linear = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM)
|
||
}
|
||
if !setOccultationPathExactWidth(points, index, widthAt, cache) ||
|
||
math.Abs(points[index].WidthKM-linear) > occultationPathWidthToleranceKM {
|
||
withinTolerance = false
|
||
}
|
||
}
|
||
anchors := append([]int{left}, indices...)
|
||
anchors = append(anchors, right)
|
||
if withinTolerance {
|
||
for index := 1; index < len(anchors); index++ {
|
||
interpolateOccultationPathWidths(points, anchors[index-1], anchors[index])
|
||
}
|
||
return
|
||
}
|
||
for index := 1; index < len(anchors); index++ {
|
||
refine(anchors[index-1], anchors[index], depth+1)
|
||
}
|
||
}
|
||
refine(0, len(points)-1, 0)
|
||
}
|
||
|
||
func uniqueOccultationPathWidthIndices(left, right int) []int {
|
||
indices := make([]int, 0, 3)
|
||
for _, numerator := range []int{1, 2, 3} {
|
||
index := left + (right-left)*numerator/4
|
||
if index <= left || index >= right || len(indices) > 0 && index == indices[len(indices)-1] {
|
||
continue
|
||
}
|
||
indices = append(indices, index)
|
||
}
|
||
return indices
|
||
}
|
||
|
||
func setOccultationPathExactWidth(
|
||
points []OccultationPathPoint,
|
||
index int,
|
||
widthAt occultationPathWidthFunc,
|
||
cache map[int]bool,
|
||
) bool {
|
||
if ok, found := cache[index]; found {
|
||
return ok
|
||
}
|
||
width, ok := widthAt(centerTimeTT(points[index].Time))
|
||
if ok && finite(width) && width >= 0 {
|
||
points[index].WidthKM = width
|
||
} else {
|
||
ok = false
|
||
}
|
||
cache[index] = ok
|
||
return ok
|
||
}
|
||
|
||
func interpolateOccultationPathWidths(points []OccultationPathPoint, left, right int) {
|
||
if right-left <= 1 {
|
||
return
|
||
}
|
||
leftTime := points[left].Time
|
||
duration := points[right].Time.Sub(leftTime).Seconds()
|
||
for index := left + 1; index < right; index++ {
|
||
fraction := float64(index-left) / float64(right-left)
|
||
if duration != 0 {
|
||
fraction = points[index].Time.Sub(leftTime).Seconds() / duration
|
||
}
|
||
points[index].WidthKM = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM)
|
||
}
|
||
}
|
||
|
||
func occultationPathDistanceKM(a, b OccultationPathPoint) float64 {
|
||
return occultationPathDistanceKMValues(a.Longitude, a.Latitude, b.Longitude, b.Latitude)
|
||
}
|
||
|
||
func occultationPathDistanceKMValues(lon1, lat1, lon2, lat2 float64) float64 {
|
||
lat1 *= math.Pi / 180
|
||
lat2 *= math.Pi / 180
|
||
dLat := lat2 - lat1
|
||
dLon := (lon2 - lon1) * math.Pi / 180
|
||
dLon = math.Mod(dLon+math.Pi, 2*math.Pi)
|
||
if dLon < 0 {
|
||
dLon += 2 * math.Pi
|
||
}
|
||
dLon -= math.Pi
|
||
h := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2)
|
||
return 2 * occultationPathEarthEquatorialRadiusKM * math.Asin(math.Sqrt(math.Min(1, h)))
|
||
}
|
||
|
||
type occultationPathVector struct{ x, y, z float64 }
|
||
|
||
func occultationPathRaDecVector(ra, dec, distance float64) occultationPathVector {
|
||
ra *= math.Pi / 180
|
||
dec *= math.Pi / 180
|
||
return occultationPathVector{
|
||
x: distance * math.Cos(dec) * math.Cos(ra),
|
||
y: distance * math.Cos(dec) * math.Sin(ra),
|
||
z: distance * math.Sin(dec),
|
||
}
|
||
}
|
||
|
||
func occultationPathAdd(a, b occultationPathVector) occultationPathVector {
|
||
return occultationPathVector{x: a.x + b.x, y: a.y + b.y, z: a.z + b.z}
|
||
}
|
||
|
||
func occultationPathSub(a, b occultationPathVector) occultationPathVector {
|
||
return occultationPathVector{x: a.x - b.x, y: a.y - b.y, z: a.z - b.z}
|
||
}
|
||
|
||
func occultationPathScale(a occultationPathVector, scalar float64) occultationPathVector {
|
||
return occultationPathVector{x: a.x * scalar, y: a.y * scalar, z: a.z * scalar}
|
||
}
|
||
|
||
func occultationPathDot(a, b occultationPathVector) float64 {
|
||
return a.x*b.x + a.y*b.y + a.z*b.z
|
||
}
|
||
|
||
func occultationPathCross(a, b occultationPathVector) occultationPathVector {
|
||
return occultationPathVector{
|
||
x: a.y*b.z - a.z*b.y,
|
||
y: a.z*b.x - a.x*b.z,
|
||
z: a.x*b.y - a.y*b.x,
|
||
}
|
||
}
|
||
|
||
func occultationPathNorm(value occultationPathVector) float64 {
|
||
return math.Sqrt(occultationPathDot(value, value))
|
||
}
|
||
|
||
func occultationPathUnit(value occultationPathVector) occultationPathVector {
|
||
norm := occultationPathNorm(value)
|
||
if norm <= 0 {
|
||
return occultationPathVector{}
|
||
}
|
||
return occultationPathScale(value, 1/norm)
|
||
}
|
||
|
||
func occultationEarthLineIntersection(origin, direction occultationPathVector) (occultationPathVector, float64, bool) {
|
||
return occultationEarthLineIntersectionWithTolerance(origin, direction, 0)
|
||
}
|
||
|
||
// occultationPathTrackReference 将影轴地面轨迹连续延伸到掠过阶段 /
|
||
// occultationPathTrackReference extends the shadow-axis ground track through grazing phases.
|
||
// 在地球外时,将椭球度量下最近点径向投影到表面,并在相切处与真实近侧交点连续连接 /
|
||
// Outside the Earth, the closest point under the ellipsoid metric is projected radially onto the surface and joined continuously to the real near-side intersection at tangency.
|
||
func occultationPathTrackReference(frame occultationPathFrame) (occultationPathVector, bool) {
|
||
if point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis); ok {
|
||
return point, true
|
||
}
|
||
|
||
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
|
||
a := frame.axis.x*frame.axis.x + frame.axis.y*frame.axis.y + frame.axis.z*frame.axis.z/polarRatioSquared
|
||
b := frame.moon.x*frame.axis.x + frame.moon.y*frame.axis.y + frame.moon.z*frame.axis.z/polarRatioSquared
|
||
if a <= 0 || !finite(a) || !finite(b) {
|
||
return occultationPathVector{}, false
|
||
}
|
||
closest := occultationPathAdd(frame.moon, occultationPathScale(frame.axis, -b/a))
|
||
metricRadius := math.Sqrt(closest.x*closest.x + closest.y*closest.y + closest.z*closest.z/polarRatioSquared)
|
||
if metricRadius <= 1e-9 || !finite(metricRadius) {
|
||
return occultationPathVector{}, false
|
||
}
|
||
return occultationPathScale(closest, occultationPathEarthEquatorialRadiusKM/metricRadius), true
|
||
}
|
||
|
||
func occultationEarthLineIntersectionWithTolerance(origin, direction occultationPathVector, tolerance float64) (occultationPathVector, float64, bool) {
|
||
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
|
||
a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared
|
||
b := origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared
|
||
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
|
||
discriminant := b*b - a*c
|
||
if discriminant < -tolerance || a <= 0 {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
if discriminant < 0 {
|
||
discriminant = 0
|
||
}
|
||
root := math.Sqrt(discriminant)
|
||
roots := [2]float64{(-b - root) / a, (-b + root) / a}
|
||
chosen := math.Inf(1)
|
||
for _, root := range roots {
|
||
if root >= 0 && root < chosen {
|
||
chosen = root
|
||
}
|
||
}
|
||
if math.IsInf(chosen, 1) {
|
||
return occultationPathVector{}, 0, false
|
||
}
|
||
return occultationPathAdd(origin, occultationPathScale(direction, chosen)), chosen, true
|
||
}
|
||
|
||
func occultationPathGeodetic(tt float64, vector occultationPathVector) (float64, float64) {
|
||
ut1 := TT2UT1(tt)
|
||
return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut1)*15)
|
||
}
|
||
|
||
func occultationPathGeodeticWithSidereal(
|
||
vector occultationPathVector,
|
||
siderealDegrees float64,
|
||
) (float64, float64) {
|
||
longitude := normalizeLongitude(math.Atan2(vector.y, vector.x)*180/math.Pi - siderealDegrees)
|
||
return longitude, occultationPathGeodeticLatitude(vector)
|
||
}
|
||
|
||
func occultationPathGeodeticLatitude(vector occultationPathVector) float64 {
|
||
return math.Atan2(
|
||
vector.z,
|
||
occultationPathEarthPolarRatio*occultationPathEarthPolarRatio*math.Hypot(vector.x, vector.y),
|
||
) * 180 / math.Pi
|
||
}
|
||
|
||
func occultationPathEarthFixedVector(tt float64, vector occultationPathVector) occultationPathVector {
|
||
return occultationPathEarthFixedVectorWithRotation(vector, occultationPathEarthRotationAt(tt))
|
||
}
|
||
|
||
type occultationPathEarthRotation struct {
|
||
cosine float64
|
||
sine float64
|
||
}
|
||
|
||
func occultationPathEarthRotationAt(tt float64) occultationPathEarthRotation {
|
||
angle := ApparentSiderealTime(TT2UT1(tt)) * 15 * math.Pi / 180
|
||
return occultationPathEarthRotation{cosine: math.Cos(angle), sine: math.Sin(angle)}
|
||
}
|
||
|
||
func occultationPathEarthFixedVectorWithRotation(
|
||
vector occultationPathVector,
|
||
rotation occultationPathEarthRotation,
|
||
) occultationPathVector {
|
||
return occultationPathVector{
|
||
x: rotation.cosine*vector.x + rotation.sine*vector.y,
|
||
y: -rotation.sine*vector.x + rotation.cosine*vector.y,
|
||
z: vector.z,
|
||
}
|
||
}
|
||
|
||
func normalizeLongitude(longitude float64) float64 {
|
||
longitude = math.Mod(longitude+180, 360)
|
||
if longitude < 0 {
|
||
longitude += 360
|
||
}
|
||
return longitude - 180
|
||
}
|