2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
1585 lines
56 KiB
Go
1585 lines
56 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 := starApparentRaDecGeocentric(tt, star)
|
|
starDistanceKM := 0.0
|
|
if star.ParallaxMas > 0 {
|
|
starDistanceKM = 206264806.247 / star.ParallaxMas * 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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(TD2UT(tt, false))*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) {
|
|
ut := TD2UT(tt, false)
|
|
return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut)*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(TD2UT(tt, false)) * 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
|
|
}
|