044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
361 lines
15 KiB
Go
361 lines
15 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// occultationLimbTarget 给出某时刻目标的地心视赤经、赤纬(度)与地心距(km,恒星为 0)及站心角半径(角秒)。
|
|
type occultationLimbTarget func(tt float64) (ra, dec, distanceKM, radiusArcsec float64, ok bool)
|
|
|
|
func planetOccultationLimbTarget(config planetOccultationConfig, observer Observer) occultationLimbTarget {
|
|
return func(tt float64) (float64, float64, float64, float64, bool) {
|
|
ra, dec := config.apparentRaDecN(tt, -1)
|
|
distanceKM := config.earthDistanceN(tt, -1) * angularDiameterAstronomicalUnitKM
|
|
radius := planetTopocentricSemidiameterN(tt, config, observer, -1)
|
|
return ra, dec, distanceKM, radius, finite(ra) && finite(dec) && finite(distanceKM) && finite(radius)
|
|
}
|
|
}
|
|
|
|
const (
|
|
occultationLimbCrossingStepDays = 60.0 / 86400.0
|
|
occultationLimbCrossingSpanDays = 3.5 / 24.0
|
|
occultationLimbRootDays = 1e-3 / 86400.0
|
|
occultationLimbMinSamples = 64
|
|
occultationLimbMaxSamples = 4000
|
|
occultationLimbBandPadArcsec = 0.05
|
|
occultationLimbUnrangedPadKM = 1.0
|
|
planetOccultationLimbStepDays = 0.25 / 86400.0
|
|
)
|
|
|
|
// occultationLimbRangeKM 返回剖面半径范围;剖面不报告范围时在 tt 时刻朝目标扫一圈并外扩 1 km。
|
|
func occultationLimbRangeKM(tt float64, limb LunarLimb, observer Observer, target occultationLimbTarget, 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
|
|
}
|
|
ra, dec, _, _, ok := target(tt)
|
|
if !ok {
|
|
return 0, 0, false
|
|
}
|
|
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
|
|
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, 1), lunarEclipseLimbVector(ra, dec, 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 - occultationLimbUnrangedPadKM, high + occultationLimbUnrangedPadKM, true
|
|
}
|
|
|
|
// occultationLimbCrossing 自 greatestTT 向 direction 一侧求圆月面间隙升到 level 的时刻;间隙自掩甚单调增大,起点已不低于 level 时返回起点。
|
|
func occultationLimbCrossing(greatestTT, direction, level float64, circular func(float64) (float64, bool)) (float64, bool) {
|
|
near := greatestTT
|
|
value, ok := circular(near)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
if value >= level {
|
|
return greatestTT, true
|
|
}
|
|
for step := 1; float64(step)*occultationLimbCrossingStepDays <= occultationLimbCrossingSpanDays; step++ {
|
|
far := greatestTT + direction*float64(step)*occultationLimbCrossingStepDays
|
|
farValue, ok := circular(far)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
if farValue < level {
|
|
near = far
|
|
continue
|
|
}
|
|
for i := 0; i < 64 && math.Abs(far-near) > occultationLimbRootDays; i++ {
|
|
mid := (near + far) / 2
|
|
midValue, ok := circular(mid)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
if midValue < level {
|
|
near = mid
|
|
} else {
|
|
far = mid
|
|
}
|
|
}
|
|
return (near + far) / 2, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// occultationLimbOuterRoots 在 [start, end] 上采样月缘间隙,返回首个由正转负与最后一个由负转正的零点;
|
|
// 端点状态由调用方保证(hiddenAtStart、hiddenAtEnd),ok 为假表示剖面无值或星历失败。
|
|
func occultationLimbOuterRoots(start, end float64, hiddenAtStart, hiddenAtEnd bool, gap func(float64) (float64, bool)) (first, last float64, found, ok bool) {
|
|
if end <= start {
|
|
return 0, 0, false, true
|
|
}
|
|
count := int(math.Ceil((end - start) / planetOccultationLimbStepDays))
|
|
if count < occultationLimbMinSamples {
|
|
count = occultationLimbMinSamples
|
|
}
|
|
if count > occultationLimbMaxSamples {
|
|
count = occultationLimbMaxSamples
|
|
}
|
|
first, last = math.NaN(), math.NaN()
|
|
previousTT, previousHidden := start, hiddenAtStart
|
|
for index := 0; index <= count; index++ {
|
|
tt := start + (end-start)*float64(index)/float64(count)
|
|
value, valueOK := gap(tt)
|
|
if !valueOK {
|
|
return 0, 0, false, false
|
|
}
|
|
hidden := value < 0
|
|
if index == 0 {
|
|
hidden = hiddenAtStart
|
|
} else if index == count {
|
|
hidden = hiddenAtEnd
|
|
}
|
|
if index > 0 && hidden != previousHidden {
|
|
root := occultationLimbBisect(previousTT, tt, previousHidden, gap)
|
|
if hidden && math.IsNaN(first) {
|
|
first = root
|
|
}
|
|
if !hidden {
|
|
last = root
|
|
}
|
|
}
|
|
previousTT, previousHidden = tt, hidden
|
|
}
|
|
return first, last, !math.IsNaN(first) || !math.IsNaN(last), true
|
|
}
|
|
|
|
// occultationLimbBisect 在相邻样本间求遮挡状态翻转的时刻;leftHidden 是左端的遮挡状态。
|
|
func occultationLimbBisect(left, right float64, leftHidden bool, gap func(float64) (float64, bool)) float64 {
|
|
for i := 0; i < 64 && right-left > occultationLimbRootDays; i++ {
|
|
mid := (left + right) / 2
|
|
value, ok := gap(mid)
|
|
if !ok {
|
|
break
|
|
}
|
|
if (value < 0) == leftHidden {
|
|
left = mid
|
|
} else {
|
|
right = mid
|
|
}
|
|
}
|
|
return (left + right) / 2
|
|
}
|
|
|
|
// planetOccultationInfoWithLimb 有月缘时按剖面判定行星月掩:只在圆月面间隙落进月缘起伏带的时段采样全方向月缘间隙,
|
|
// 取最外侧的外切与内切;月缘可新增或取消圆月面的掠掩与全掩,有正持续时间的接触一律按偏掩或全掩上报。剖面无值时退回圆月面结果。
|
|
func planetOccultationInfoWithLimb(greatestTT float64, config planetOccultationConfig, observer Observer, safetyMarginArcsec float64, location *time.Location, limb LunarLimb, inst *Instance) (PlanetOccultationInfo, bool) {
|
|
circularInfo, circularOK := planetOccultationInfoAtGreatest(greatestTT, config, observer, safetyMarginArcsec, location, inst)
|
|
if limb == nil {
|
|
return circularInfo, circularOK
|
|
}
|
|
info, state, ok := planetOccultationGreatestInfo(greatestTT, config, observer, safetyMarginArcsec, location, inst)
|
|
if !ok {
|
|
return circularInfo, circularOK
|
|
}
|
|
target := planetOccultationLimbTarget(config, observer)
|
|
lowKM, highKM, ok := occultationLimbRangeKM(greatestTT, limb, observer, target, inst)
|
|
if !ok {
|
|
return circularInfo, circularOK
|
|
}
|
|
ephemeris, ok := newPlanetLimbEphemeris(greatestTT, config, limb, inst)
|
|
if !ok {
|
|
return circularInfo, circularOK
|
|
}
|
|
// 月缘间隙 ≈ 圆月面间隙 −(月缘半径 − 参考半径),所以圆月面间隙不低于 bandHigh 必不接触、不高于 bandLow 必已接触。
|
|
// 起伏带以角秒计,外扩一点容差覆盖月心距在事件内的变化。
|
|
scale := state.moonSemidiameter / moonEquatorialRadiusKM
|
|
bandLow := (lowKM-moonEquatorialRadiusKM)*scale - occultationLimbBandPadArcsec
|
|
bandHigh := (highKM-moonEquatorialRadiusKM)*scale + occultationLimbBandPadArcsec
|
|
circular := func(internal bool) func(float64) (float64, bool) {
|
|
return func(tt float64) (float64, bool) { return ephemeris.circularGap(tt, observer, internal) }
|
|
}
|
|
limbGap := func(internal bool) func(float64) (float64, bool) {
|
|
return func(tt float64) (float64, bool) { return ephemeris.limbGap(tt, observer, internal) }
|
|
}
|
|
// solve 求一种接触的最外侧掩入、复现:圆月面间隙在 [bandLow, bandHigh] 之外的两端遮挡状态已知,只在带内采样。
|
|
solve := func(internal bool, circularMinimum float64) (float64, float64, bool, bool) {
|
|
if circularMinimum >= bandHigh {
|
|
return 0, 0, false, true
|
|
}
|
|
outerLeft, okLeft := occultationLimbCrossing(greatestTT, -1, bandHigh, circular(internal))
|
|
outerRight, okRight := occultationLimbCrossing(greatestTT, 1, bandHigh, circular(internal))
|
|
if !okLeft || !okRight {
|
|
return 0, 0, false, false
|
|
}
|
|
if circularMinimum > bandLow {
|
|
first, last, found, ok := occultationLimbOuterRoots(outerLeft, outerRight, false, false, limbGap(internal))
|
|
return first, last, found && !math.IsNaN(first) && !math.IsNaN(last), ok
|
|
}
|
|
innerLeft, okLeft := occultationLimbCrossing(greatestTT, -1, bandLow, circular(internal))
|
|
innerRight, okRight := occultationLimbCrossing(greatestTT, 1, bandLow, circular(internal))
|
|
if !okLeft || !okRight {
|
|
return 0, 0, false, false
|
|
}
|
|
first, _, foundLeft, okLeftRoots := occultationLimbOuterRoots(outerLeft, innerLeft, false, true, limbGap(internal))
|
|
_, last, foundRight, okRightRoots := occultationLimbOuterRoots(innerRight, outerRight, true, false, limbGap(internal))
|
|
return first, last, foundLeft && foundRight && !math.IsNaN(first) && !math.IsNaN(last), okLeftRoots && okRightRoots
|
|
}
|
|
externalImmersion, externalEmersion, external, ok := solve(false, state.externalContactMetric)
|
|
if !ok {
|
|
return circularInfo, circularOK
|
|
}
|
|
if !external {
|
|
return PlanetOccultationInfo{}, false
|
|
}
|
|
internalImmersion, internalEmersion, internal, ok := solve(true, state.internalContactMetric)
|
|
if !ok {
|
|
return circularInfo, circularOK
|
|
}
|
|
info.Type = OccultationPartial
|
|
info.ExternalImmersion = occultationTTToLocation(externalImmersion, location)
|
|
info.ExternalEmersion = occultationTTToLocation(externalEmersion, location)
|
|
if internal && internalImmersion > externalImmersion && internalEmersion < externalEmersion && internalImmersion < internalEmersion {
|
|
info.Type = OccultationTotal
|
|
info.HasInternalContacts = true
|
|
info.InternalImmersion = occultationTTToLocation(internalImmersion, location)
|
|
info.InternalEmersion = occultationTTToLocation(internalEmersion, location)
|
|
}
|
|
info.ContactsComplete = true
|
|
info.LunarLimbApplied = true
|
|
return info, true
|
|
}
|
|
|
|
// planetLimbEphemeris 掩甚前后 ±4 h 的地心月球、行星视位置与月球本体坐标架插值表;站心换算逐点精确计算。
|
|
type planetLimbEphemeris struct {
|
|
bodies []localEphemerisVectorNode
|
|
axes []localEphemerisVectorNode
|
|
base lunarLimbFrame
|
|
radiusKM float64
|
|
}
|
|
|
|
func newPlanetLimbEphemeris(greatestTT float64, config planetOccultationConfig, limb LunarLimb, inst *Instance) (*planetLimbEphemeris, bool) {
|
|
half := int(math.Round(starOccultationLimbNodeSpan / starOccultationLimbNodeDays))
|
|
ephemeris := &planetLimbEphemeris{
|
|
bodies: make([]localEphemerisVectorNode, 2*half+1),
|
|
axes: make([]localEphemerisVectorNode, 2*half+1),
|
|
radiusKM: config.equatorialRadiusKM,
|
|
}
|
|
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))
|
|
planetRA, planetDec := config.apparentRaDecN(tt, -1)
|
|
planet := lunarEclipseLimbVector(planetRA, planetDec, config.earthDistanceN(tt, -1)*angularDiameterAstronomicalUnitKM)
|
|
frame := newLunarLimbFrame(limb, tt, moon, inst)
|
|
if !frame.valid || !finiteVector3(moon) || !finiteVector3(planet) {
|
|
return nil, false
|
|
}
|
|
if index == half {
|
|
ephemeris.base = frame
|
|
}
|
|
ephemeris.bodies[index] = localEphemerisVectorNode{tt: tt, first: moon, next: planet}
|
|
ephemeris.axes[index] = localEphemerisVectorNode{tt: tt, first: frame.axisX, next: frame.axisZ}
|
|
}
|
|
return ephemeris, true
|
|
}
|
|
|
|
// planetLimbTopocentric 是插值得到的站心月球、行星方向与距离(km),以及该时刻的月球本体坐标架。
|
|
type planetLimbTopocentric struct {
|
|
moon, planet [3]float64
|
|
moonDistance, planetRadius float64
|
|
frame lunarLimbFrame
|
|
}
|
|
|
|
func (ephemeris *planetLimbEphemeris) topocentricAt(tt float64, observer Observer) (planetLimbTopocentric, bool) {
|
|
moon, planet, ok := interpolateLocalEphemerisVectors(ephemeris.bodies, tt)
|
|
if !ok {
|
|
return planetLimbTopocentric{}, false
|
|
}
|
|
axisX, axisZ, ok := interpolateLocalEphemerisVectors(ephemeris.axes, tt)
|
|
if !ok {
|
|
return planetLimbTopocentric{}, false
|
|
}
|
|
ut := TT2UTC(tt)
|
|
toTopocentric := func(vector [3]float64) (float64, float64, float64, bool) {
|
|
distance := math.Sqrt(lunarLimbDot(vector, vector))
|
|
ra := normalizeRA(math.Atan2(vector[1], vector[0]) / rad)
|
|
dec := math.Asin(clampUnit(vector[2]/distance)) / rad
|
|
topoRA, topoDec := TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distance/angularDiameterAstronomicalUnitKM, observer.Height)
|
|
topoDistance := topocentricDistanceKM(ra, dec, distance, observer, ut)
|
|
return normalizeRA(topoRA), topoDec, topoDistance, finite(topoRA) && finite(topoDec) && finite(topoDistance) && topoDistance > 0
|
|
}
|
|
moonRA, moonDec, moonDistance, okMoon := toTopocentric(moon)
|
|
planetRA, planetDec, planetDistance, okPlanet := toTopocentric(planet)
|
|
if !okMoon || !okPlanet || planetDistance <= ephemeris.radiusKM {
|
|
return planetLimbTopocentric{}, false
|
|
}
|
|
frame := ephemeris.base
|
|
if frame.axisZ, ok = lunarLimbNormalize(axisZ); !ok {
|
|
return planetLimbTopocentric{}, false
|
|
}
|
|
if frame.axisX, ok = lunarLimbNormalize(lunarLimbReject(axisX, frame.axisZ)); !ok {
|
|
return planetLimbTopocentric{}, false
|
|
}
|
|
frame.axisY = lunarLimbCross(frame.axisZ, frame.axisX)
|
|
return planetLimbTopocentric{
|
|
moon: lunarEclipseLimbVector(moonRA, moonDec, moonDistance),
|
|
planet: lunarEclipseLimbVector(planetRA, planetDec, 1),
|
|
moonDistance: moonDistance,
|
|
planetRadius: math.Asin(ephemeris.radiusKM / planetDistance),
|
|
frame: frame,
|
|
}, true
|
|
}
|
|
|
|
// circularGap 圆月面外切或内切间隙(角秒),与 planetOccultationStateAt 同口径。
|
|
func (ephemeris *planetLimbEphemeris) circularGap(tt float64, observer Observer, internal bool) (float64, bool) {
|
|
state, ok := ephemeris.topocentricAt(tt, observer)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
separation := math.Acos(clampUnit(lunarLimbDot(state.moon, state.planet) / state.moonDistance))
|
|
moonRadius := math.Asin(moonEquatorialRadiusKM / state.moonDistance)
|
|
if internal {
|
|
return (separation - moonRadius + state.planetRadius) * angularDiameterArcsecPerRadian, true
|
|
}
|
|
return (separation - moonRadius - state.planetRadius) * angularDiameterArcsecPerRadian, true
|
|
}
|
|
|
|
// limbGap 全方向月缘外切或内切间隙(角秒)。
|
|
func (ephemeris *planetLimbEphemeris) limbGap(tt float64, observer Observer, internal bool) (float64, bool) {
|
|
state, ok := ephemeris.topocentricAt(tt, observer)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
view, ok := state.frame.viewToward(state.moon, state.planet)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
contact := lunarLimbContact{
|
|
frame: &state.frame,
|
|
view: view,
|
|
distanceKM: state.moonDistance,
|
|
separation: math.Acos(clampUnit(lunarLimbDot(state.moon, state.planet) / state.moonDistance)),
|
|
targetRadius: state.planetRadius,
|
|
}
|
|
if internal {
|
|
inner, ok := contact.innerRadius(true)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return (contact.separation - math.Abs(inner-contact.targetRadius)) * angularDiameterArcsecPerRadian, true
|
|
}
|
|
outer, ok := contact.outerRadius()
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return (contact.separation - outer - contact.targetRadius) * angularDiameterArcsecPerRadian, true
|
|
}
|