2026-08-06 12:00:56 +08:00
package basic
import (
"math"
"sort"
2026-09-17 12:27:40 +08:00
"sync"
2026-08-06 12:00:56 +08:00
"time"
)
const (
2026-09-17 12:27:40 +08:00
// 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
2026-08-06 12:00:56 +08:00
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 {
2026-09-17 12:27:40 +08:00
path , ok , err := starOccultationPathAtSeed ( seedTT , star , options , start , end )
2026-08-06 12:00:56 +08:00
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 {
2026-09-17 12:27:40 +08:00
if options . Algorithm == "" {
options . Algorithm = OccultationPathAlgorithmOptimized
}
2026-08-06 12:00:56 +08:00
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
}
2026-09-17 12:27:40 +08:00
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
}
2026-08-06 12:00:56 +08:00
return options
}
func starOccultationPathAtSeed (
seedTT float64 ,
star StarCoordinate ,
options OccultationPathOptions ,
2026-09-17 12:27:40 +08:00
selectionStart , selectionEnd time . Time ,
2026-08-06 12:00:56 +08:00
) ( StarOccultationPath , bool , error ) {
2026-09-17 12:27:40 +08:00
location := selectionStart . Location ()
cache := newStarOccultationEventCache ( star )
cache . prepareLocalEphemeris ( seedTT )
frameAt := cache . frameAt
candidateFrameAt := cache . candidateFrameAt
2026-08-06 12:00:56 +08:00
searchStart := seedTT - occultationPathSearchSpanDays
searchEnd := seedTT + occultationPathSearchSpanDays
2026-09-17 12:27:40 +08:00
outerStart , outerEnd , ok := starOccultationPathWindowWithCandidateFrames (
seedTT , searchStart , searchEnd , candidateFrameAt , frameAt , false ,
)
2026-08-06 12:00:56 +08:00
if ! ok {
return StarOccultationPath {}, false , nil
}
2026-09-17 12:27:40 +08:00
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 )
2026-08-06 12:00:56 +08:00
if ! greatestOK {
if hasCenter {
greatestTT = math . Max ( centerStart , math . Min ( centerEnd , greatestTT ))
2026-09-17 12:27:40 +08:00
greatest , greatestOK = starOccultationPathCenterPointWithFrame ( greatestTT , frameAt , location )
2026-08-06 12:00:56 +08:00
}
}
if ! greatestOK {
2026-09-17 12:27:40 +08:00
// 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 )
2026-08-06 12:00:56 +08:00
}
if ! greatestOK {
return StarOccultationPath {}, false , nil
}
2026-09-17 12:27:40 +08:00
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
}
2026-08-06 12:00:56 +08:00
2026-09-17 12:27:40 +08:00
start := starOccultationPathBoundaryEndpointWithFrame ( outerStart , frameAt , location , 1 )
end := starOccultationPathBoundaryEndpointWithFrame ( outerEnd , frameAt , location , - 1 )
2026-08-06 12:00:56 +08:00
if ! start . valid || ! end . valid {
return StarOccultationPath {}, false , nil
}
2026-09-17 12:27:40 +08:00
exactFrameAt := frameAt
if options . Algorithm != OccultationPathAlgorithmExact {
optimized := newStarOccultationEventCache ( star )
optimized . preparePathEphemeris ( seedTT , options . Algorithm )
if optimized . local . dense {
cache = optimized
frameAt = cache . frameAt
}
}
2026-08-06 12:00:56 +08:00
path := StarOccultationPath {
TargetID : star . ID ,
Start : start . point ,
Greatest : greatest ,
End : end . point ,
Complete : outerStart > searchStart && outerEnd < searchEnd ,
Step : options . Step ,
TargetSpacingKM : options . TargetSpacingKM ,
}
2026-09-17 12:27:40 +08:00
centerLine , northern , southern , err := starOccultationPathSamplesWithFrame (
2026-08-06 12:00:56 +08:00
outerStart ,
outerEnd ,
centerStart ,
centerEnd ,
hasCenter ,
greatestTT ,
2026-09-17 12:27:40 +08:00
frameAt ,
2026-08-06 12:00:56 +08:00
options ,
location ,
)
if err != nil {
return StarOccultationPath {}, false , err
}
2026-09-17 12:27:40 +08:00
if cache . local . dense {
correctOccultationCenterWidths ( centerLine , exactFrameAt )
}
2026-08-06 12:00:56 +08:00
path . CenterLine = centerLine
path . NorthernLimit = occultationPathWithEndpoints ( start . point , end . point , northern )
path . SouthernLimit = occultationPathWithEndpoints ( start . point , end . point , southern )
2026-09-17 12:27:40 +08:00
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 ,
)
2026-08-06 12:00:56 +08:00
return path , true , nil
}
2026-09-17 12:27:40 +08:00
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
}
2026-08-06 12:00:56 +08:00
func occultationPathWithEndpoints ( start , end OccultationPathPoint , points [] OccultationPathPoint ) [] OccultationPathPoint {
result := make ([] OccultationPathPoint , 0 , len ( points ) + 2 )
result = append ( result , start )
for _ , point := range points {
2026-09-17 12:27:40 +08:00
if point . Time . After ( result [ len ( result ) - 1 ]. Time ) && point . Time . Before ( end . Time ) {
2026-08-06 12:00:56 +08:00
result = append ( result , point )
}
}
return append ( result , end )
}
2026-09-17 12:27:40 +08:00
// occultationPathLimitSeparations 在同一时刻的南北限采样对上填写地面间距,并返回最接近掩甚的那一对的间距。
func occultationPathLimitSeparations (
north , south [] OccultationPathPoint ,
greatestTT float64 ,
) ( float64 , bool ) {
if len ( north ) == 0 || len ( north ) != len ( south ) {
return 0 , false
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
separation , found := 0.0 , false
nearestDelta := math . Inf ( 1 )
for index := range north {
if ! north [ index ]. Time . Equal ( south [ index ]. Time ) {
continue
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
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
2026-08-06 12:00:56 +08:00
}
}
2026-09-17 12:27:40 +08:00
return separation , found
}
2026-08-06 12:00:56 +08:00
2026-09-17 12:27:40 +08:00
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
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
contour := append ([] OccultationPathPoint ( nil ), source [ sampleRange . start : sampleRange . end ] ... )
contours = append ( contours , contour )
2026-08-06 12:00:56 +08:00
}
}
2026-09-17 12:27:40 +08:00
return contours
}
2026-08-06 12:00:56 +08:00
2026-09-17 12:27:40 +08:00
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 ]) {
2026-08-06 12:00:56 +08:00
continue
}
2026-09-17 12:27:40 +08:00
ranges = append ( ranges , occultationPathSampleRange { start : start , end : index })
start = index
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
return append ( ranges , occultationPathSampleRange { start : start , end : len ( points )})
}
func occultationPathBoundaryBranchChanged ( first , second OccultationPathPoint ) bool {
distance := occultationPathDistanceKM ( first , second )
if distance <= occultationPathBoundaryBranchJumpKM {
return false
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
duration := math . Abs ( second . Time . Sub ( first . Time ). Seconds ())
return duration == 0 || distance / duration > occultationPathBoundaryBranchSpeedKMPerSecond
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
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
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
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
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
return hasBoundary ( frame )
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
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 )
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
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 )
2026-08-06 12:00:56 +08:00
if ! ok {
return math . Inf ( 1 )
}
return math . Hypot ( frame . moonProjectionX (), frame . moonProjectionY ())
}
2026-09-17 12:27:40 +08:00
func starOccultationPathSamplesWithFrame (
2026-08-06 12:00:56 +08:00
outerStartTT , outerEndTT float64 ,
centerStartTT , centerEndTT float64 ,
hasCenter bool ,
greatestTT float64 ,
2026-09-17 12:27:40 +08:00
frameAt occultationPathFrameFunc ,
2026-08-06 12:00:56 +08:00
options OccultationPathOptions ,
location * time . Location ,
) ([] OccultationPathPoint , [] OccultationPathPoint , [] OccultationPathPoint , error ) {
var points [] OccultationPathPoint
if hasCenter {
var err error
2026-09-17 12:27:40 +08:00
points , err = starOccultationPathCenterSamplesWithFrame ( centerStartTT , centerEndTT , greatestTT , frameAt , options , location )
2026-08-06 12:00:56 +08:00
if err != nil {
return nil , nil , nil , err
}
}
northern , southern := occultationPathBoundarySamplesForFrame (
2026-09-17 12:27:40 +08:00
outerStartTT , outerEndTT , greatestTT , frameAt , occultationPathContourStepDays ( options ), location ,
2026-08-06 12:00:56 +08:00
)
return points , northern , southern , nil
}
2026-09-17 12:27:40 +08:00
func starOccultationPathCenterSamplesWithFrame (
2026-08-06 12:00:56 +08:00
startTT , endTT , greatestTT float64 ,
2026-09-17 12:27:40 +08:00
frameAt occultationPathFrameFunc ,
2026-08-06 12:00:56 +08:00
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 {
2026-09-17 12:27:40 +08:00
point , ok := starOccultationPathCenterPointWithFrame ( tt , frameAt , location )
if ok && ( len ( points ) == 0 || point . Time . After ( points [ len ( points ) - 1 ]. Time )) {
2026-08-06 12:00:56 +08:00
points = append ( points , point )
}
}
if options . TargetSpacingKM > 0 {
2026-09-17 12:27:40 +08:00
return refineOccultationPathSpacingWithFrame ( points , frameAt , options . TargetSpacingKM , location )
2026-08-06 12:00:56 +08:00
}
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
}
2026-09-17 12:27:40 +08:00
engine := occultationMovingDiskEngine ()
engine . maxSampleCount = maximumCount
times , _ := engine . sampleTimes ( startTT , endTT , greatestTT , stepDays )
return times
2026-08-06 12:00:56 +08:00
}
2026-09-17 12:27:40 +08:00
func refineOccultationPathSpacingWithFrame (
2026-08-06 12:00:56 +08:00
points [] OccultationPathPoint ,
2026-09-17 12:27:40 +08:00
frameAt occultationPathFrameFunc ,
2026-08-06 12:00:56 +08:00
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 ) {
2026-09-17 12:27:40 +08:00
_ , _ , width , ok := occultationPathLimitsAndWidthForFrame ( tt , frameAt )
2026-08-06 12:00:56 +08:00
return width , ok
}
for i := 1 ; i < len ( points ); i ++ {
segmentStart := len ( refined ) - 1
var err error
2026-09-17 12:27:40 +08:00
refined , err = appendOccultationPathSegmentWithFrame ( refined , points [ i - 1 ], points [ i ], frameAt , targetSpacingKM , location , 0 )
2026-08-06 12:00:56 +08:00
if err != nil {
return nil , err
}
refineOccultationPathWidths ( refined [ segmentStart :], widthAt )
}
return refined , nil
}
2026-09-17 12:27:40 +08:00
func appendOccultationPathSegmentWithFrame (
2026-08-06 12:00:56 +08:00
points [] OccultationPathPoint ,
start , end OccultationPathPoint ,
2026-09-17 12:27:40 +08:00
frameAt occultationPathFrameFunc ,
2026-08-06 12:00:56 +08:00
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
2026-09-17 12:27:40 +08:00
midTime := occultationTTToLocation ( midTT , location )
if ! midTime . After ( start . Time ) || ! midTime . Before ( end . Time ) {
return append ( points , end ), nil
}
mid , ok := starOccultationPathCenterPointWithoutWidthWithFrame ( midTT , frameAt , location )
2026-08-06 12:00:56 +08:00
if ! ok {
return append ( points , end ), nil
}
mid . WidthKM = ( start . WidthKM + end . WidthKM ) / 2
var err error
2026-09-17 12:27:40 +08:00
points , err = appendOccultationPathSegmentWithFrame ( points , start , mid , frameAt , targetSpacingKM , location , depth + 1 )
2026-08-06 12:00:56 +08:00
if err != nil {
return nil , err
}
2026-09-17 12:27:40 +08:00
return appendOccultationPathSegmentWithFrame ( points , mid , end , frameAt , targetSpacingKM , location , depth + 1 )
2026-08-06 12:00:56 +08:00
}
func uniqueOccultationPathTimes ( times [] float64 ) [] float64 {
2026-09-17 12:27:40 +08:00
return movingDiskUniqueTimes ( times )
2026-08-06 12:00:56 +08:00
}
type occultationPathFrame struct {
moon occultationPathVector
axis occultationPathVector
first occultationPathVector
second occultationPathVector
moonRadius float64
targetRadius float64
2026-09-17 12:27:40 +08:00
// 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 )
2026-08-06 12:00:56 +08:00
}
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 ) {
2026-09-17 12:27:40 +08:00
return starOccultationPathFrameFromState ( starOccultationEphemerisStateAt ( tt , star ))
}
func starOccultationEphemerisStateAt ( tt float64 , star StarCoordinate ) starOccultationEphemerisState {
2026-08-06 12:00:56 +08:00
moonRA , moonDec := HMoonGeocentricApparentRaDecN ( tt , - 1 )
2026-09-17 12:27:40 +08:00
moonDistanceKM := HMoonAwayN ( tt , - 1 )
2026-09-23 18:55:12 +08:00
// 方向与距离必须来自同一次三维推进,否则视差修正会退回历元距离。
starRA , starDec , starDistanceAU := starApparentRaDecDistanceGeocentric ( tt , star )
starDistanceKM := starDistanceAU * occultationPathAstronomicalUnitKM
2026-09-17 12:27:40 +08:00
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 {
2026-08-06 12:00:56 +08:00
return occultationPathFrame {}, false
}
2026-09-17 12:27:40 +08:00
moon := occultationPathRaDecVector ( state . moonRA , state . moonDec , state . moonDistanceKM )
starDirection := occultationPathRaDecVector ( state . starRA , state . starDec , 1 )
2026-08-06 12:00:56 +08:00
axis := occultationPathScale ( starDirection , - 1 )
2026-09-17 12:27:40 +08:00
if state . starDistanceKM > 0 {
target := occultationPathRaDecVector ( state . starRA , state . starDec , state . starDistanceKM )
2026-08-06 12:00:56 +08:00
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 ,
2026-09-17 12:27:40 +08:00
moonRadius : math . Asin ( moonEquatorialRadiusKM / state . moonDistanceKM ),
2026-08-06 12:00:56 +08:00
}, true
}
func starOccultationPathHasBoundary ( frame occultationPathFrame ) bool {
_ , _ , ok := occultationPathBoundaryTangent ( frame )
return ok
}
2026-09-17 12:27:40 +08:00
func starOccultationPathBoundaryEndpointWithFrame (
tt float64 ,
frameAt occultationPathFrameFunc ,
location * time . Location ,
direction int ,
) occultationPathEndpoint {
if _ , ok := frameAt ( tt ); ! ok {
2026-08-06 12:00:56 +08:00
return occultationPathEndpoint {}
}
for offset := 0 ; offset <= 3 ; offset ++ {
candidateTT := tt + float64 ( direction ) * float64 ( offset ) * 0.5 / 86400.0
2026-09-17 12:27:40 +08:00
candidateFrame , candidateOK := frameAt ( candidateTT )
2026-08-06 12:00:56 +08:00
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 ) {
2026-09-17 12:27:40 +08:00
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 )
2026-08-06 12:00:56 +08:00
if ! ok {
return OccultationPathPoint {}, false
}
point , _ , ok := occultationEarthLineIntersection ( frame . moon , frame . axis )
if ! ok {
return OccultationPathPoint {}, false
}
width := 0.0
2026-09-17 12:27:40 +08:00
if _ , _ , tangentWidth , limitsOK := occultationPathLimitsAndWidthForFrame ( tt , frameAt ); limitsOK {
2026-08-06 12:00:56 +08:00
width = tangentWidth
}
return occultationPathPointFromVectorWithMoon ( tt , point , width , frame . moon , location ), true
}
2026-09-17 12:27:40 +08:00
func starOccultationPathCenterPointWithoutWidthWithFrame (
tt float64 ,
frameAt occultationPathFrameFunc ,
location * time . Location ,
) ( OccultationPathPoint , bool ) {
frame , ok := frameAt ( tt )
2026-08-06 12:00:56 +08:00
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 ) {
2026-09-17 12:27:40 +08:00
latitude := occultationPathGeodeticLatitude ( vector )
2026-08-06 12:00:56 +08:00
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 )
}
}
2026-09-23 18:55:12 +08:00
// 同 occultationPathFiniteCrossTrackExtrema:等差末样本与 rightTheta 差 1 ULP,
// 掠射时端点会被判"无地面交点"而丢掉极值,端点原值必须补采一次。
if vector , _ , ok := occultationPathBoundaryVector ( frame , rightTheta ); ok {
consider ( vector )
}
2026-08-06 12:00:56 +08:00
}
}
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 ) {
2026-09-17 12:27:40 +08:00
if cached , hit := occultationPathCachedThetaInterval ( frame , centerTheta ); hit {
return cached . left , cached . right , cached . ok
}
2026-08-06 12:00:56 +08:00
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 )
2026-09-17 12:27:40 +08:00
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 )
2026-08-06 12:00:56 +08:00
}
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
}
2026-09-17 12:27:40 +08:00
sineTheta , cosineTheta := math . Sincos ( theta )
2026-08-06 12:00:56 +08:00
radial := occultationPathAdd (
2026-09-17 12:27:40 +08:00
occultationPathScale ( frame . first , cosineTheta ),
occultationPathScale ( frame . second , sineTheta ),
2026-08-06 12:00:56 +08:00
)
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 ) {
2026-09-17 12:27:40 +08:00
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 ) {
2026-08-06 12:00:56 +08:00
step := 2 * math . Pi / float64 ( occultationPathBoundaryScanPoints )
2026-09-17 12:27:40 +08:00
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 ) {
2026-08-06 12:00:56 +08:00
bestTheta := 0.0
bestDiscriminant := math . Inf ( - 1 )
2026-09-17 12:27:40 +08:00
for i , discriminant := range discriminants {
if discriminant > bestDiscriminant {
2026-08-06 12:00:56 +08:00
bestDiscriminant = discriminant
2026-09-17 12:27:40 +08:00
bestTheta = step * float64 ( i )
2026-08-06 12:00:56 +08:00
}
}
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 {
2026-09-17 12:27:40 +08:00
return occultationPathPointFromVectorWithMoonSidereal (
2026-09-23 18:55:12 +08:00
tt , vector , width , moon , ApparentSiderealTime ( TT2UT1 ( tt )) * 15 , location ,
2026-09-17 12:27:40 +08:00
)
}
func occultationPathPointFromVectorWithMoonSidereal (
tt float64 ,
vector occultationPathVector ,
width float64 ,
moon occultationPathVector ,
siderealDegrees float64 ,
location * time . Location ,
) OccultationPathPoint {
lon , lat := occultationPathGeodeticWithSidereal ( vector , siderealDegrees )
2026-08-06 12:00:56 +08:00
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
2026-09-17 12:27:40 +08:00
moonRA , moonDec = topocentricRaDecWithSidereal (
moonRA , moonDec , lat , lon , siderealDegrees ,
2026-08-06 12:00:56 +08:00
moonDistance / occultationPathAstronomicalUnitKM , 0 ,
)
return OccultationPathPoint {
2026-09-17 12:27:40 +08:00
Time : occultationTTToLocation ( tt , location ),
Longitude : lon ,
Latitude : lat ,
MoonAltitude : occultationAltitudeWithSidereal (
siderealDegrees , Observer { Longitude : lon , Latitude : lat }, normalizeRA ( moonRA ), moonDec ,
),
WidthKM : width ,
2026-08-06 12:00:56 +08:00
}
}
2026-09-17 12:27:40 +08:00
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
}
2026-08-06 12:00:56 +08:00
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 ) {
2026-09-23 18:55:12 +08:00
ut1 := TT2UT1 ( tt )
return occultationPathGeodeticWithSidereal ( vector , ApparentSiderealTime ( ut1 ) * 15 )
2026-09-17 12:27:40 +08:00
}
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 (
2026-08-06 12:00:56 +08:00
vector . z ,
occultationPathEarthPolarRatio * occultationPathEarthPolarRatio * math . Hypot ( vector . x , vector . y ),
) * 180 / math . Pi
}
func occultationPathEarthFixedVector ( tt float64 , vector occultationPathVector ) occultationPathVector {
2026-09-17 12:27:40 +08:00
return occultationPathEarthFixedVectorWithRotation ( vector , occultationPathEarthRotationAt ( tt ))
}
type occultationPathEarthRotation struct {
cosine float64
sine float64
}
func occultationPathEarthRotationAt ( tt float64 ) occultationPathEarthRotation {
2026-09-23 18:55:12 +08:00
angle := ApparentSiderealTime ( TT2UT1 ( tt )) * 15 * math . Pi / 180
2026-09-17 12:27:40 +08:00
return occultationPathEarthRotation { cosine : math . Cos ( angle ), sine : math . Sin ( angle )}
}
func occultationPathEarthFixedVectorWithRotation (
vector occultationPathVector ,
rotation occultationPathEarthRotation ,
) occultationPathVector {
2026-08-06 12:00:56 +08:00
return occultationPathVector {
2026-09-17 12:27:40 +08:00
x : rotation . cosine * vector . x + rotation . sine * vector . y ,
y : - rotation . sine * vector . x + rotation . cosine * vector . y ,
2026-08-06 12:00:56 +08:00
z : vector . z ,
}
}
func normalizeLongitude ( longitude float64 ) float64 {
longitude = math . Mod ( longitude + 180 , 360 )
if longitude < 0 {
longitude += 360
}
return longitude - 180
}