feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user