044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
462 lines
19 KiB
Go
462 lines
19 KiB
Go
package basic
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"sort"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
starOccultationLimbDefaultStep = 200 * time.Millisecond
|
||
starOccultationLimbMinimumStep = 10 * time.Millisecond
|
||
starOccultationLimbMaxSamples = 20000
|
||
starOccultationLimbMinSamples = 64
|
||
starOccultationLimbBoundStep = 60.0 / 86400.0
|
||
starOccultationLimbBoundSpan = 3.5 / 24.0
|
||
starOccultationLimbNodeDays = 10.0 / 1440.0
|
||
starOccultationLimbNodeSpan = 4.0 / 24.0
|
||
starOccultationLimbRootDays = 1e-3 / 86400.0
|
||
starOccultationLimbUnrangedPadKM = 1.0
|
||
)
|
||
|
||
// StarOccultationLimbProfileOptions 控制精密恒星月掩的星迹采样 / sampling of a precise stellar occultation.
|
||
type StarOccultationLimbProfileOptions struct {
|
||
// Step 是星迹采样步长;零值 200 ms,小于 10 ms 提升到 10 ms;每段至少 64 个样本、总数不超过 20000,越界时自动调整。短于步长的遮挡或复现可能漏检 / track sampling step.
|
||
Step time.Duration
|
||
}
|
||
|
||
// StarOccultationLimbSample 星迹上一个时刻的月缘几何,角量均为角秒、度 / limb geometry at one instant on the star track.
|
||
type StarOccultationLimbSample struct {
|
||
Time time.Time
|
||
// SeparationArcsec 恒星到站心月心的角距 / star–Moon-centre separation.
|
||
SeparationArcsec float64
|
||
// LimbRadiusArcsec 恒星方向上的月缘角半径 / limb radius toward the star.
|
||
LimbRadiusArcsec float64
|
||
// MeanRadiusArcsec 参考圆月面角半径 / circular reference radius.
|
||
MeanRadiusArcsec float64
|
||
// PositionAngleDeg 恒星相对月心的位置角,自天北向东 / position angle of the star.
|
||
PositionAngleDeg float64
|
||
// Hidden 恒星位于月缘以内 / star is behind the limb.
|
||
Hidden bool
|
||
}
|
||
|
||
// OccultationInterval 一段连续遮挡 / one continuous hidden interval.
|
||
type OccultationInterval struct {
|
||
Start time.Time
|
||
End time.Time
|
||
}
|
||
|
||
// StarOccultationLimbProfileResult 一次恒星月掩沿星迹的月缘剖面与全部遮挡区间 / limb profile and every hidden interval of a stellar occultation.
|
||
type StarOccultationLimbProfileResult struct {
|
||
// Occultation 按同一剖面求得的标准结果,掩始、掩终为首次遮挡与最后复现 / standard result on the same profile.
|
||
Occultation StarOccultationInfo
|
||
// Segments 月缘起伏带内的采样段,按时间排序;深掩时为掩入侧与复现侧两段,掠掩时通常为一段 / samples inside the limb relief band.
|
||
Segments [][]StarOccultationLimbSample
|
||
// Hidden 全部遮挡区间,按时间排序 / every hidden interval.
|
||
Hidden []OccultationInterval
|
||
// ReferenceRadiusKM 样本 MeanRadiusArcsec 对应的参考月球半径 / reference lunar radius behind MeanRadiusArcsec.
|
||
ReferenceRadiusKM float64
|
||
// ReliefMinKM / ReliefMaxKM 判定所用的月缘半径范围 / limb radius range used for gating.
|
||
ReliefMinKM float64
|
||
ReliefMaxKM float64
|
||
// Step 实际采用的采样步长 / effective step.
|
||
Step time.Duration
|
||
}
|
||
|
||
type starLimbState struct {
|
||
separation, limbRadius, meanRadius, positionAngle float64
|
||
distanceKM float64
|
||
}
|
||
|
||
// starLimbEphemeris 掩甚前后 ±4 h 的地心月球、恒星视位置与月球本体坐标架插值表;站心换算逐点精确计算。
|
||
type starLimbEphemeris struct {
|
||
bodies []localEphemerisVectorNode
|
||
axes []localEphemerisVectorNode
|
||
base lunarLimbFrame
|
||
hasDistance bool
|
||
}
|
||
|
||
func newStarLimbEphemeris(greatestTT float64, star StarCoordinate, limb LunarLimb, inst *Instance) (*starLimbEphemeris, bool) {
|
||
half := int(math.Round(starOccultationLimbNodeSpan / starOccultationLimbNodeDays))
|
||
ephemeris := &starLimbEphemeris{
|
||
bodies: make([]localEphemerisVectorNode, 2*half+1),
|
||
axes: make([]localEphemerisVectorNode, 2*half+1),
|
||
}
|
||
for index := range ephemeris.bodies {
|
||
tt := greatestTT + float64(index-half)*starOccultationLimbNodeDays
|
||
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
|
||
moon := lunarEclipseLimbVector(moonRA, moonDec, HMoonAwayNWithInstance(tt, -1, inst))
|
||
starRA, starDec, distanceAU := starApparentRaDecDistanceGeocentric(tt, star, inst)
|
||
if distanceAU <= 0 {
|
||
distanceAU = 1
|
||
} else {
|
||
ephemeris.hasDistance = true
|
||
}
|
||
frame := newLunarLimbFrame(limb, tt, moon, inst)
|
||
if !frame.valid || !finiteVector3(moon) {
|
||
return nil, false
|
||
}
|
||
if index == half {
|
||
ephemeris.base = frame
|
||
}
|
||
ephemeris.bodies[index] = localEphemerisVectorNode{tt: tt, first: moon, next: lunarEclipseLimbVector(starRA, starDec, distanceAU)}
|
||
ephemeris.axes[index] = localEphemerisVectorNode{tt: tt, first: frame.axisX, next: frame.axisZ}
|
||
}
|
||
return ephemeris, true
|
||
}
|
||
|
||
func (ephemeris *starLimbEphemeris) stateAt(tt float64, observer Observer) (starLimbState, bool) {
|
||
moon, starVector, ok := interpolateLocalEphemerisVectors(ephemeris.bodies, tt)
|
||
if !ok {
|
||
return starLimbState{}, false
|
||
}
|
||
axisX, axisZ, ok := interpolateLocalEphemerisVectors(ephemeris.axes, tt)
|
||
if !ok {
|
||
return starLimbState{}, false
|
||
}
|
||
geoDistance := math.Sqrt(lunarLimbDot(moon, moon))
|
||
geoRA := normalizeRA(math.Atan2(moon[1], moon[0]) / rad)
|
||
geoDec := math.Asin(clampUnit(moon[2]/geoDistance)) / rad
|
||
ut := TT2UTC(tt)
|
||
topoRA, topoDec := TopocentricRaDec(geoRA, geoDec, observer.Latitude, observer.Longitude, ut, geoDistance/149597870.7, observer.Height)
|
||
topoRA = normalizeRA(topoRA)
|
||
topoDistance := topocentricDistanceKM(geoRA, geoDec, geoDistance, observer, ut)
|
||
starDistance := math.Sqrt(lunarLimbDot(starVector, starVector))
|
||
starRA := normalizeRA(math.Atan2(starVector[1], starVector[0]) / rad)
|
||
starDec := math.Asin(clampUnit(starVector[2]/starDistance)) / rad
|
||
if ephemeris.hasDistance {
|
||
starRA, starDec = TopocentricRaDec(starRA, starDec, observer.Latitude, observer.Longitude, ut, starDistance, observer.Height)
|
||
starRA = normalizeRA(starRA)
|
||
}
|
||
if !finite(topoRA) || !finite(topoDec) || !finite(topoDistance) || topoDistance <= 0 || !finite(starRA) || !finite(starDec) {
|
||
return starLimbState{}, false
|
||
}
|
||
frame := ephemeris.base
|
||
if frame.axisX, ok = lunarLimbNormalize(axisX); !ok {
|
||
return starLimbState{}, false
|
||
}
|
||
if frame.axisZ, ok = lunarLimbNormalize(axisZ); !ok {
|
||
return starLimbState{}, false
|
||
}
|
||
// 两轴分别插值后不再严格正交,按 z 轴重新正交化 x 轴。
|
||
if frame.axisX, ok = lunarLimbNormalize(lunarLimbReject(frame.axisX, frame.axisZ)); !ok {
|
||
return starLimbState{}, false
|
||
}
|
||
frame.axisY = lunarLimbCross(frame.axisZ, frame.axisX)
|
||
radiusKM, ok := frame.radiusKM(lunarEclipseLimbVector(topoRA, topoDec, topoDistance), lunarEclipseLimbVector(starRA, starDec, 1), false)
|
||
if !ok {
|
||
return starLimbState{}, false
|
||
}
|
||
return starLimbState{
|
||
separation: angularSeparationDegrees(topoRA, topoDec, starRA, starDec) * 3600,
|
||
limbRadius: angularSemidiameterArcsec(radiusKM, topoDistance),
|
||
meanRadius: angularSemidiameterArcsec(moonEquatorialRadiusKM, topoDistance),
|
||
positionAngle: occultationPositionAngle(topoRA, topoDec, starRA, starDec),
|
||
distanceKM: topoDistance,
|
||
}, true
|
||
}
|
||
|
||
// starOccultationLimbRange 返回剖面半径范围;剖面不报告范围时在掩甚扫一圈并外扩 1 km。
|
||
func starOccultationLimbRange(tt float64, star StarCoordinate, observer Observer, limb LunarLimb, inst *Instance) (float64, float64, bool) {
|
||
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
|
||
geoDistance := HMoonAwayNWithInstance(tt, -1, inst)
|
||
frame := newLunarLimbFrame(limb, tt, lunarEclipseLimbVector(geoRA, geoDec, geoDistance), inst)
|
||
if !frame.valid {
|
||
return 0, 0, false
|
||
}
|
||
if frame.ranged {
|
||
return frame.minKM, frame.maxKM, true
|
||
}
|
||
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
|
||
starRA, starDec := starApparentRaDec(tt, star, observer, inst)
|
||
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, 1), lunarEclipseLimbVector(starRA, starDec, 1))
|
||
if !ok {
|
||
return 0, 0, false
|
||
}
|
||
low, high := math.Inf(1), math.Inf(-1)
|
||
for offset := 0.0; offset < 360; offset += frame.stepDeg {
|
||
radius, ok := frame.radiusAt(view, offset)
|
||
if !ok {
|
||
return 0, 0, false
|
||
}
|
||
low, high = math.Min(low, radius), math.Max(high, radius)
|
||
}
|
||
return low - starOccultationLimbUnrangedPadKM, high + starOccultationLimbUnrangedPadKM, true
|
||
}
|
||
|
||
// starOccultationSeparationCrossing 自掩甚向 direction 一侧求角距升到 radiusKM 对应角半径的时刻;角距自掩甚单调增大。
|
||
func starOccultationSeparationCrossing(greatestTT float64, direction float64, radiusKM float64, ephemeris *starLimbEphemeris, observer Observer) (float64, bool) {
|
||
value := func(tt float64) float64 {
|
||
state, ok := ephemeris.stateAt(tt, observer)
|
||
if !ok {
|
||
return math.NaN()
|
||
}
|
||
return state.separation - angularSemidiameterArcsec(radiusKM, state.distanceKM)
|
||
}
|
||
near, nearValue := greatestTT, value(greatestTT)
|
||
if !finite(nearValue) {
|
||
return 0, false
|
||
}
|
||
if nearValue >= 0 {
|
||
return greatestTT, true
|
||
}
|
||
for step := 1; float64(step)*starOccultationLimbBoundStep <= starOccultationLimbBoundSpan; step++ {
|
||
far := greatestTT + direction*float64(step)*starOccultationLimbBoundStep
|
||
farValue := value(far)
|
||
if !finite(farValue) {
|
||
return 0, false
|
||
}
|
||
if farValue < 0 {
|
||
near, nearValue = far, farValue
|
||
continue
|
||
}
|
||
for i := 0; i < 64 && math.Abs(far-near) > starOccultationLimbRootDays; i++ {
|
||
mid := (near + far) / 2
|
||
if v := value(mid); v < 0 {
|
||
near = mid
|
||
} else {
|
||
far = mid
|
||
}
|
||
}
|
||
return (near + far) / 2, true
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
type starLimbRoot struct {
|
||
tt float64
|
||
disappear bool
|
||
}
|
||
|
||
type starLimbScan struct {
|
||
segments [][]StarOccultationLimbSample
|
||
hidden [][2]float64
|
||
lowKM float64
|
||
highKM float64
|
||
stepDays float64
|
||
// grazing 为真表示星迹始终在月缘起伏带内(单段采样)。
|
||
grazing bool
|
||
}
|
||
|
||
// starOccultationLimbScanAt 按剖面起伏带把星迹分必见、必掩与带内三段,带外两端遮挡状态由构造保证;ok 为假表示剖面无值或星历失败。
|
||
func starOccultationLimbScanAt(greatestTT float64, star StarCoordinate, observer Observer, limb LunarLimb, stepDays float64, location *time.Location, inst *Instance) (starLimbScan, bool) {
|
||
lowKM, highKM, ok := starOccultationLimbRange(greatestTT, star, observer, limb, inst)
|
||
if !ok {
|
||
return starLimbScan{}, false
|
||
}
|
||
scan := starLimbScan{lowKM: lowKM, highKM: highKM, stepDays: stepDays}
|
||
ephemeris, ok := newStarLimbEphemeris(greatestTT, star, limb, inst)
|
||
if !ok {
|
||
return starLimbScan{}, false
|
||
}
|
||
outerLeft, okLeft := starOccultationSeparationCrossing(greatestTT, -1, highKM, ephemeris, observer)
|
||
outerRight, okRight := starOccultationSeparationCrossing(greatestTT, 1, highKM, ephemeris, observer)
|
||
if !okLeft || !okRight {
|
||
return starLimbScan{}, false
|
||
}
|
||
if outerLeft == greatestTT && outerRight == greatestTT {
|
||
return scan, true
|
||
}
|
||
innerLeft, okLeft := starOccultationSeparationCrossing(greatestTT, -1, lowKM, ephemeris, observer)
|
||
innerRight, okRight := starOccultationSeparationCrossing(greatestTT, 1, lowKM, ephemeris, observer)
|
||
if !okLeft || !okRight {
|
||
return starLimbScan{}, false
|
||
}
|
||
type span struct {
|
||
start, end float64
|
||
hiddenAtStart bool
|
||
hiddenAtEnd bool
|
||
forceStart, forceEnd bool
|
||
}
|
||
spans := []span{{outerLeft, innerLeft, false, true, true, true}, {innerRight, outerRight, true, false, true, true}}
|
||
if innerLeft == greatestTT && innerRight == greatestTT {
|
||
spans = []span{{outerLeft, outerRight, false, false, true, true}}
|
||
}
|
||
total, shortest := 0.0, math.Inf(1)
|
||
for _, item := range spans {
|
||
total += item.end - item.start
|
||
shortest = math.Min(shortest, item.end-item.start)
|
||
}
|
||
// 步长过大时掠掩两端都可见、中间的遮挡会整段漏掉,所以每段至少保留 64 个样本。
|
||
if limit := shortest / starOccultationLimbMinSamples; shortest > 0 && stepDays > limit {
|
||
stepDays = math.Max(limit, starOccultationLimbMinimumStep.Hours()/24)
|
||
}
|
||
if total/stepDays > starOccultationLimbMaxSamples {
|
||
stepDays = total / starOccultationLimbMaxSamples
|
||
}
|
||
scan.stepDays = stepDays
|
||
scan.grazing = len(spans) == 1
|
||
gapAt := func(tt float64) (starLimbState, float64, bool) {
|
||
state, ok := ephemeris.stateAt(tt, observer)
|
||
return state, state.separation - state.limbRadius, ok
|
||
}
|
||
var roots []starLimbRoot
|
||
for _, item := range spans {
|
||
// 剖面范围零宽时带也零长,遮挡状态在该点翻转;不产样本,避免同刻样本。
|
||
if item.end-item.start < starOccultationLimbRootDays {
|
||
if item.hiddenAtStart != item.hiddenAtEnd {
|
||
roots = append(roots, starLimbRoot{tt: item.start, disappear: item.hiddenAtEnd})
|
||
}
|
||
continue
|
||
}
|
||
count := int(math.Ceil((item.end - item.start) / stepDays))
|
||
if count < 1 {
|
||
count = 1
|
||
}
|
||
samples := make([]StarOccultationLimbSample, 0, count+1)
|
||
previousTT, previousHidden := 0.0, false
|
||
var previousGap float64
|
||
for index := 0; index <= count; index++ {
|
||
tt := item.start + (item.end-item.start)*float64(index)/float64(count)
|
||
state, gap, ok := gapAt(tt)
|
||
if !ok {
|
||
return starLimbScan{}, false
|
||
}
|
||
hidden := gap < 0
|
||
// 带两端的遮挡状态由角距保证,消除端点上的舍入抖动。
|
||
if index == 0 && item.forceStart {
|
||
hidden = item.hiddenAtStart
|
||
}
|
||
if index == count && item.forceEnd {
|
||
hidden = item.hiddenAtEnd
|
||
}
|
||
samples = append(samples, StarOccultationLimbSample{
|
||
Time: occultationTTToLocation(tt, location),
|
||
SeparationArcsec: state.separation,
|
||
LimbRadiusArcsec: state.limbRadius,
|
||
MeanRadiusArcsec: state.meanRadius,
|
||
PositionAngleDeg: state.positionAngle,
|
||
Hidden: hidden,
|
||
})
|
||
if index > 0 && hidden != previousHidden {
|
||
root := starOccultationLimbBisect(previousTT, tt, previousGap, gap, gapAt)
|
||
roots = append(roots, starLimbRoot{tt: root, disappear: hidden})
|
||
}
|
||
previousTT, previousHidden, previousGap = tt, hidden, gap
|
||
}
|
||
scan.segments = append(scan.segments, samples)
|
||
}
|
||
sort.SliceStable(roots, func(i, j int) bool { return roots[i].tt < roots[j].tt })
|
||
start := math.NaN()
|
||
for _, root := range roots {
|
||
if root.disappear {
|
||
start = root.tt
|
||
} else if !math.IsNaN(start) {
|
||
scan.hidden = append(scan.hidden, [2]float64{start, root.tt})
|
||
start = math.NaN()
|
||
}
|
||
}
|
||
return scan, true
|
||
}
|
||
|
||
// starOccultationLimbBisect 在相邻样本间求月缘间隙零点;端点被强制定号时零点可能落在端点上。
|
||
func starOccultationLimbBisect(left, right, leftGap, rightGap float64, gapAt func(float64) (starLimbState, float64, bool)) float64 {
|
||
if leftGap*rightGap > 0 {
|
||
if math.Abs(leftGap) < math.Abs(rightGap) {
|
||
return left
|
||
}
|
||
return right
|
||
}
|
||
for i := 0; i < 64 && right-left > starOccultationLimbRootDays; i++ {
|
||
mid := (left + right) / 2
|
||
_, gap, ok := gapAt(mid)
|
||
if !ok {
|
||
break
|
||
}
|
||
if (gap < 0) == (leftGap < 0) {
|
||
left, leftGap = mid, gap
|
||
} else {
|
||
right = mid
|
||
}
|
||
}
|
||
return (left + right) / 2
|
||
}
|
||
|
||
// starOccultationInfoWithLimb 有月缘时按剖面判定是否成掩,掩始、掩终取首次遮挡与最后复现;剖面无值时退回圆月面。
|
||
func starOccultationInfoWithLimb(greatestTT float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, location *time.Location, limb LunarLimb, inst *Instance) (StarOccultationInfo, bool) {
|
||
if limb == nil {
|
||
return starOccultationInfoAtGreatest(greatestTT, star, observer, safetyMarginArcsec, location, inst)
|
||
}
|
||
info, ok := starOccultationGreatestInfo(greatestTT, star, observer, safetyMarginArcsec, location, inst)
|
||
if !ok {
|
||
return StarOccultationInfo{}, false
|
||
}
|
||
scan, ok := starOccultationLimbScanAt(greatestTT, star, observer, limb, starOccultationLimbDefaultStep.Hours()/24, location, inst)
|
||
if !ok {
|
||
return starOccultationInfoAtGreatest(greatestTT, star, observer, safetyMarginArcsec, location, inst)
|
||
}
|
||
return starOccultationInfoFromScan(info, scan, location), len(scan.hidden) > 0
|
||
}
|
||
|
||
func starOccultationInfoFromScan(info StarOccultationInfo, scan starLimbScan, location *time.Location) StarOccultationInfo {
|
||
if len(scan.hidden) == 0 {
|
||
info.Type = OccultationNone
|
||
info.Immersion, info.Emersion = time.Time{}, time.Time{}
|
||
info.ContactsComplete = false
|
||
info.LunarLimbApplied = true
|
||
return info
|
||
}
|
||
info.Type = OccultationTotal
|
||
if scan.grazing {
|
||
info.Type = OccultationGrazing
|
||
}
|
||
info.Immersion = occultationTTToLocation(scan.hidden[0][0], location)
|
||
info.Emersion = occultationTTToLocation(scan.hidden[len(scan.hidden)-1][1], location)
|
||
info.ContactsComplete = true
|
||
info.LunarLimbApplied = true
|
||
return info
|
||
}
|
||
|
||
// StarOccultationLimbProfileWithInstance 沿星迹按月缘剖面精密求解一次恒星月掩,返回全部遮挡区间与带内采样;info 只取掩甚时刻与观测者,须为 UTC 口径结果(不能传 UT1 标注副本),恒星未被遮挡时 Occultation.Type 为 OccultationNone / precise stellar occultation along the track with a lunar limb profile.
|
||
func StarOccultationLimbProfileWithInstance(info StarOccultationInfo, star StarCoordinate, limb LunarLimb, options StarOccultationLimbProfileOptions, inst *Instance) (StarOccultationLimbProfileResult, error) {
|
||
if limb == nil {
|
||
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: lunar limb profile is required", ErrInvalidOccultationInput)
|
||
}
|
||
if err := star.Validate(); err != nil {
|
||
return StarOccultationLimbProfileResult{}, err
|
||
}
|
||
if err := info.Observer.Validate(); err != nil {
|
||
return StarOccultationLimbProfileResult{}, err
|
||
}
|
||
if info.Greatest.IsZero() {
|
||
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: greatest occultation time is required", ErrInvalidOccultationInput)
|
||
}
|
||
if options.Step < 0 {
|
||
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: profile step cannot be negative", ErrInvalidOccultationInput)
|
||
}
|
||
step := options.Step
|
||
if step == 0 {
|
||
step = starOccultationLimbDefaultStep
|
||
}
|
||
if step < starOccultationLimbMinimumStep {
|
||
step = starOccultationLimbMinimumStep
|
||
}
|
||
location := info.Greatest.Location()
|
||
greatestTT := occultationTimeToTT(info.Greatest)
|
||
base, ok := starOccultationGreatestInfo(greatestTT, star, info.Observer, math.Inf(1), location, inst)
|
||
if !ok {
|
||
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: geometry is unavailable at greatest occultation", ErrInvalidOccultationInput)
|
||
}
|
||
base.TargetID = star.ID
|
||
scan, ok := starOccultationLimbScanAt(greatestTT, star, info.Observer, limb, step.Hours()/24, location, inst)
|
||
if !ok {
|
||
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: lunar limb profile has no value along the track", ErrInvalidOccultationInput)
|
||
}
|
||
profile := StarOccultationLimbProfileResult{
|
||
Occultation: starOccultationInfoFromScan(base, scan, location),
|
||
Segments: scan.segments,
|
||
ReferenceRadiusKM: moonEquatorialRadiusKM,
|
||
ReliefMinKM: scan.lowKM,
|
||
ReliefMaxKM: scan.highKM,
|
||
Step: time.Duration(math.Round(scan.stepDays * 86400 * float64(time.Second))),
|
||
}
|
||
for _, interval := range scan.hidden {
|
||
profile.Hidden = append(profile.Hidden, OccultationInterval{
|
||
Start: occultationTTToLocation(interval[0], location),
|
||
End: occultationTTToLocation(interval[1], location),
|
||
})
|
||
}
|
||
return profile, nil
|
||
}
|