package basic import ( "math" "sort" "time" ) const ( occultationPathDefaultStepDays = 1.0 / 1440.0 occultationPathMinStepDays = 1.0 / 86400.0 occultationPathMaxSampleCount = 30000 occultationPathMaxAdaptiveDepth = 20 occultationPathBoundarySpacingKM = 500.0 occultationPathVelocityStepDays = 1.0 / 1440.0 occultationPathBoundaryScanPoints = 720 occultationPathRootToleranceDays = occultationEventSelectionToleranceDays occultationPathRangeStepDays = 5.0 / 1440.0 occultationPathSearchSpanDays = 2.0 occultationPathWidthToleranceKM = 0.005 occultationPathEarthEquatorialRadiusKM = 6378.1366 occultationPathEarthPolarRatio = 0.99664719 occultationPathAstronomicalUnitKM = 149597870.7 ) // FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带。 // 查询窗口按全球几何掩甚点选择事件,端点容差为 10 ms,与数值根精度一致。返回路径扩展到完整全球起止点;函数不会加载内嵌星表,调用者需显式提供坐标。 // FindStarOccultationPaths searches the global lunar-occultation footprint of one point-source star. // The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end; the function does not load the embedded catalog. func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) { if err := validateOccultationTimeRange(start, end); err != nil { return nil, err } if err := star.Validate(); err != nil { return nil, err } if err := options.Validate(); err != nil { return nil, err } options = normalizeOccultationPathOptions(options) startTT := occultationTimeToTT(start) endTT := occultationTimeToTT(end) candidateStartTT := startTT - occultationPathSearchSpanDays candidateEndTT := endTT + occultationPathSearchSpanDays coarseOptions := OccultationSearchOptions{} candidates := starOccultationGeocentricCandidateGreatestTimes( candidateStartTT, candidateEndTT, starOccultationCoarseStepDays(coarseOptions), star, 0, ) paths := make([]StarOccultationPath, 0, len(candidates)) for _, seedTT := range candidates { path, ok, err := starOccultationPathAtSeed(seedTT, star, options, start.Location()) if err != nil { return nil, err } if !ok { continue } if !occultationTimeInSelectionWindow(path.Greatest.Time, start, end) { continue } if len(paths) > 0 && math.Abs(paths[len(paths)-1].Greatest.Time.Sub(path.Greatest.Time).Seconds()) <= 60 { continue } paths = append(paths, path) } sort.SliceStable(paths, func(i, j int) bool { return paths[i].Greatest.Time.Before(paths[j].Greatest.Time) }) return paths, nil } func normalizeOccultationPathOptions(options OccultationPathOptions) OccultationPathOptions { if options.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 } return options } func starOccultationPathAtSeed( seedTT float64, star StarCoordinate, options OccultationPathOptions, location *time.Location, ) (StarOccultationPath, bool, error) { searchStart := seedTT - occultationPathSearchSpanDays searchEnd := seedTT + occultationPathSearchSpanDays outerStart, outerEnd, ok := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, false) if !ok { return StarOccultationPath{}, false, nil } centerStart, centerEnd, hasCenter := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, true) greatestTT := starOccultationPathGreatest(seedTT, outerStart, outerEnd, star) greatest, greatestOK := starOccultationPathCenterPoint(greatestTT, star, location) if !greatestOK { if hasCenter { greatestTT = math.Max(centerStart, math.Min(centerEnd, greatestTT)) greatest, greatestOK = starOccultationPathCenterPoint(greatestTT, star, location) } } if !greatestOK { frameAt := func(tt float64) (occultationPathFrame, bool) { return starOccultationPathFrameAt(tt, star) } greatest, greatestOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, location) } if !greatestOK { return StarOccultationPath{}, false, nil } start := starOccultationPathBoundaryEndpoint(outerStart, star, location, 1) end := starOccultationPathBoundaryEndpoint(outerEnd, star, location, -1) if !start.valid || !end.valid { return StarOccultationPath{}, false, nil } path := StarOccultationPath{ TargetID: star.ID, Start: start.point, Greatest: greatest, End: end.point, Complete: outerStart > searchStart && outerEnd < searchEnd, Step: options.Step, TargetSpacingKM: options.TargetSpacingKM, } centerLine, northern, southern, err := starOccultationPathSamples( outerStart, outerEnd, centerStart, centerEnd, hasCenter, greatestTT, star, options, location, ) if err != nil { return StarOccultationPath{}, false, err } path.CenterLine = centerLine path.NorthernLimit = occultationPathWithEndpoints(start.point, end.point, northern) path.SouthernLimit = occultationPathWithEndpoints(start.point, end.point, southern) return path, true, nil } func occultationPathWithEndpoints(start, end OccultationPathPoint, points []OccultationPathPoint) []OccultationPathPoint { result := make([]OccultationPathPoint, 0, len(points)+2) result = append(result, start) for _, point := range points { if point.Time.After(start.Time) && point.Time.Before(end.Time) { result = append(result, point) } } return append(result, end) } func starOccultationPathWindow(seedTT, startTT, endTT float64, star StarCoordinate, center bool) (float64, float64, bool) { left := math.Max(startTT, seedTT-occultationPathSearchSpanDays) right := math.Min(endTT, seedTT+occultationPathSearchSpanDays) if right <= left { return 0, 0, false } predicate := func(tt float64) bool { frame, ok := starOccultationPathFrameAt(tt, star) if !ok { return false } if center { _, _, ok = occultationEarthLineIntersection(frame.moon, frame.axis) return ok } return starOccultationPathHasBoundary(frame) } step := occultationPathRangeStepDays first := math.NaN() previous := left previousOK := predicate(previous) if previousOK { first = previous } else { for tt := left + step; tt <= right; tt += step { current := math.Min(tt, right) currentOK := predicate(current) if currentOK { first = occultationPathRefineTransition(previous, current, predicate, false) break } previous = current previousOK = currentOK } } if math.IsNaN(first) { return 0, 0, false } last := first previous = first previousOK = true for tt := first + step; tt <= right; tt += step { current := math.Min(tt, right) currentOK := predicate(current) if !currentOK { last = occultationPathRefineTransition(previous, current, predicate, true) return first, last, true } last = current previous = current previousOK = currentOK } if previousOK { last = right } return first, last, true } func occultationPathRefineTransition(left, right float64, predicate func(float64) bool, trueToFalse bool) float64 { leftOK := predicate(left) for i := 0; i < 48 && math.Abs(right-left) > occultationPathRootToleranceDays; i++ { mid := (left + right) / 2 midOK := predicate(mid) if trueToFalse { if midOK { left = mid } else { right = mid } continue } if midOK { right = mid } else { left = mid } } if trueToFalse { return left } if leftOK { return left } return right } func starOccultationPathGreatest(seedTT, startTT, endTT float64, star StarCoordinate) float64 { left := math.Max(startTT, seedTT-0.75) right := math.Min(endTT, seedTT+0.75) if right <= left { return seedTT } const goldenRatio = 0.6180339887498949 x1 := right - goldenRatio*(right-left) x2 := left + goldenRatio*(right-left) f1 := starOccultationPathImpact(x1, star) f2 := starOccultationPathImpact(x2, star) for i := 0; i < 56; i++ { if f1 > f2 { left = x1 x1 = x2 f1 = f2 x2 = left + goldenRatio*(right-left) f2 = starOccultationPathImpact(x2, star) } else { right = x2 x2 = x1 f2 = f1 x1 = right - goldenRatio*(right-left) f1 = starOccultationPathImpact(x1, star) } } return (left + right) / 2 } func starOccultationPathImpact(tt float64, star StarCoordinate) float64 { frame, ok := starOccultationPathFrameAt(tt, star) if !ok { return math.Inf(1) } return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY()) } func starOccultationPathSamples( outerStartTT, outerEndTT float64, centerStartTT, centerEndTT float64, hasCenter bool, greatestTT float64, star StarCoordinate, options OccultationPathOptions, location *time.Location, ) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) { var points []OccultationPathPoint if hasCenter { var err error points, err = starOccultationPathCenterSamples(centerStartTT, centerEndTT, greatestTT, star, options, location) if err != nil { return nil, nil, nil, err } } frameAt := func(tt float64) (occultationPathFrame, bool) { return starOccultationPathFrameAt(tt, star) } northern, southern := occultationPathBoundarySamplesForFrame( outerStartTT, outerEndTT, greatestTT, frameAt, options, location, ) return points, northern, southern, nil } func starOccultationPathCenterSamples( startTT, endTT, greatestTT float64, star StarCoordinate, options OccultationPathOptions, location *time.Location, ) ([]OccultationPathPoint, error) { stepDays := float64(options.Step) / float64(24*time.Hour) times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays) points := make([]OccultationPathPoint, 0, len(times)) for _, tt := range times { point, ok := starOccultationPathCenterPoint(tt, star, location) if ok { points = append(points, point) } } if options.TargetSpacingKM > 0 { return refineOccultationPathSpacing(points, star, options.TargetSpacingKM, location) } return points, nil } func occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays float64) []float64 { return occultationPathSampleTimesWithLimit( startTT, endTT, greatestTT, stepDays, occultationPathMaxSampleCount, ) } func occultationPathSampleTimesWithLimit( startTT, endTT, greatestTT, stepDays float64, maximumCount int, ) []float64 { if endTT < startTT { startTT, endTT = endTT, startTT } if maximumCount < 3 { maximumCount = 3 } duration := endTT - startTT if duration <= 0 { return []float64{startTT} } if stepDays <= 0 || !finite(stepDays) { stepDays = duration } baseSampleCount := int(math.Ceil(duration/stepDays)) + 1 if baseSampleCount+1 > maximumCount { times := []float64{startTT, greatestTT, endTT} interiorCount := maximumCount - len(times) for index := 1; index <= interiorCount; index++ { times = append(times, startTT+duration*float64(index)/float64(interiorCount+1)) } sort.Float64s(times) return uniqueOccultationPathTimes(times) } times := []float64{startTT, greatestTT, endTT} for index := 1; ; index++ { tt := startTT + float64(index)*stepDays if tt >= endTT { break } times = append(times, tt) } sort.Float64s(times) return uniqueOccultationPathTimes(times) } func refineOccultationPathSpacing( points []OccultationPathPoint, star StarCoordinate, targetSpacingKM float64, location *time.Location, ) ([]OccultationPathPoint, error) { if len(points) < 2 || targetSpacingKM <= 0 { return points, nil } refined := make([]OccultationPathPoint, 0, len(points)) refined = append(refined, points[0]) widthAt := func(tt float64) (float64, bool) { _, _, width, ok := starOccultationPathLimitsAndWidthAt(tt, star) return width, ok } for i := 1; i < len(points); i++ { segmentStart := len(refined) - 1 var err error refined, err = appendOccultationPathSegment(refined, points[i-1], points[i], star, targetSpacingKM, location, 0) if err != nil { return nil, err } refineOccultationPathWidths(refined[segmentStart:], widthAt) } return refined, nil } func appendOccultationPathSegment( points []OccultationPathPoint, start, end OccultationPathPoint, star StarCoordinate, 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 mid, ok := starOccultationPathCenterPointWithoutWidth(midTT, star, location) if !ok { return append(points, end), nil } mid.WidthKM = (start.WidthKM + end.WidthKM) / 2 var err error points, err = appendOccultationPathSegment(points, start, mid, star, targetSpacingKM, location, depth+1) if err != nil { return nil, err } return appendOccultationPathSegment(points, mid, end, star, targetSpacingKM, location, depth+1) } func uniqueOccultationPathTimes(times []float64) []float64 { if len(times) < 2 { return times } unique := times[:1] for _, tt := range times[1:] { if math.Abs(tt-unique[len(unique)-1]) <= 1e-10 { continue } unique = append(unique, tt) } return unique } type occultationPathFrame struct { moon occultationPathVector axis occultationPathVector first occultationPathVector second occultationPathVector moonRadius float64 targetRadius float64 } 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) { moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1) moonDistance := HMoonAwayN(tt, -1) if !finite(moonRA) || !finite(moonDec) || !finite(moonDistance) || moonDistance <= 0 { return occultationPathFrame{}, false } moon := occultationPathRaDecVector(moonRA, moonDec, moonDistance) starRA, starDec := starApparentRaDecGeocentric(tt, star) starDirection := occultationPathRaDecVector(starRA, starDec, 1) axis := occultationPathScale(starDirection, -1) if star.ParallaxMas > 0 { distanceAU := 206264806.247 / star.ParallaxMas target := occultationPathRaDecVector(starRA, starDec, distanceAU*occultationPathAstronomicalUnitKM) axis = occultationPathScale(occultationPathSub(target, moon), -1) } axis = occultationPathUnit(axis) north := occultationPathVector{z: 1} first := occultationPathCross(north, axis) if occultationPathNorm(first) < 1e-12 { first = occultationPathCross(occultationPathVector{x: 1}, axis) } first = occultationPathUnit(first) second := occultationPathUnit(occultationPathCross(axis, first)) return occultationPathFrame{ moon: moon, axis: axis, first: first, second: second, moonRadius: MoonSemidiameter(tt) * math.Pi / (180 * 3600), }, true } func starOccultationPathHasBoundary(frame occultationPathFrame) bool { _, _, ok := occultationPathBoundaryTangent(frame) return ok } func starOccultationPathBoundaryEndpoint(tt float64, star StarCoordinate, location *time.Location, direction int) occultationPathEndpoint { if _, ok := starOccultationPathFrameAt(tt, star); !ok { return occultationPathEndpoint{} } for offset := 0; offset <= 3; offset++ { candidateTT := tt + float64(direction)*float64(offset)*0.5/86400.0 candidateFrame, candidateOK := starOccultationPathFrameAt(candidateTT, star) 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) { frame, ok := starOccultationPathFrameAt(tt, star) if !ok { return OccultationPathPoint{}, false } point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis) if !ok { return OccultationPathPoint{}, false } width := 0.0 if _, _, tangentWidth, limitsOK := starOccultationPathLimitsAndWidthAt(tt, star); limitsOK { width = tangentWidth } return occultationPathPointFromVectorWithMoon(tt, point, width, frame.moon, location), true } func starOccultationPathCenterPointWithoutWidth(tt float64, star StarCoordinate, location *time.Location) (OccultationPathPoint, bool) { frame, ok := starOccultationPathFrameAt(tt, star) 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 starOccultationPathLimitsAndWidthAt(tt float64, star StarCoordinate) (occultationPathVector, occultationPathVector, float64, bool) { frameAt := func(candidateTT float64) (occultationPathFrame, bool) { return starOccultationPathFrameAt(candidateTT, star) } return occultationPathLimitsAndWidthForFrame(tt, frameAt) } func occultationPathScannedLimitsAtFrame(tt float64, frame occultationPathFrame) (occultationPathVector, occultationPathVector, bool) { var northern, southern occultationPathVector northLatitude := math.Inf(-1) southLatitude := math.Inf(1) consider := func(vector occultationPathVector) { _, latitude := occultationPathGeodetic(tt, vector) if latitude > northLatitude { northLatitude = latitude northern = vector } if latitude < southLatitude { southLatitude = latitude southern = vector } } if tangentPoint, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame); tangentOK { consider(tangentPoint) if leftTheta, rightTheta, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta); intervalOK { const intervalSamples = 128 for i := 0; i <= intervalSamples; i++ { theta := leftTheta + (rightTheta-leftTheta)*float64(i)/intervalSamples if vector, _, ok := occultationPathBoundaryVector(frame, theta); ok { consider(vector) } } } } for i := 0; i < occultationPathBoundaryScanPoints; i++ { vector, _, ok := occultationPathBoundaryVector(frame, 2*math.Pi*float64(i)/float64(occultationPathBoundaryScanPoints)) if !ok { continue } consider(vector) } return northern, southern, finite(northLatitude) && finite(southLatitude) } func occultationPathBoundaryThetaInterval(frame occultationPathFrame, centerTheta float64) (float64, float64, bool) { if !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) return left, right, leftOK && rightOK && right > left } 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 } radial := occultationPathAdd( occultationPathScale(frame.first, math.Cos(theta)), occultationPathScale(frame.second, math.Sin(theta)), ) 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) { step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints) bestTheta := 0.0 bestDiscriminant := math.Inf(-1) for i := 0; i < occultationPathBoundaryScanPoints; i++ { theta := step * float64(i) discriminant, _, _, ok := occultationPathBoundaryLine(frame, theta) if ok && discriminant > bestDiscriminant { bestDiscriminant = discriminant bestTheta = theta } } 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 { lon, lat := occultationPathGeodetic(tt, vector) moonDistance := occultationPathNorm(moon) moonRA := normalizeRA(math.Atan2(moon.y, moon.x) * 180 / math.Pi) moonDec := math.Asin(math.Max(-1, math.Min(1, moon.z/moonDistance))) * 180 / math.Pi moonRA, moonDec = TopocentricRaDec( moonRA, moonDec, lat, lon, TD2UT(tt, false), moonDistance/occultationPathAstronomicalUnitKM, 0, ) return OccultationPathPoint{ Time: occultationTTToLocation(tt, location), Longitude: lon, Latitude: lat, MoonAltitude: occultationAltitude(tt, Observer{Longitude: lon, Latitude: lat}, normalizeRA(moonRA), moonDec), WidthKM: width, } } func centerTimeTT(value time.Time) float64 { return occultationTimeToTT(value) } type occultationPathWidthFunc func(float64) (float64, bool) func refineOccultationPathWidths(points []OccultationPathPoint, widthAt occultationPathWidthFunc) { if len(points) < 3 { return } cache := make(map[int]bool) var refine func(int, int, int) refine = func(left, right, depth int) { if right-left <= 1 || depth >= occultationPathMaxAdaptiveDepth { return } if right-left <= 4 { for index := left + 1; index < right; index++ { setOccultationPathExactWidth(points, index, widthAt, cache) } return } indices := uniqueOccultationPathWidthIndices(left, right) withinTolerance := true leftTime := points[left].Time duration := points[right].Time.Sub(leftTime).Seconds() for _, index := range indices { linear := (points[left].WidthKM + points[right].WidthKM) / 2 if duration != 0 { fraction := points[index].Time.Sub(leftTime).Seconds() / duration linear = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM) } if !setOccultationPathExactWidth(points, index, widthAt, cache) || math.Abs(points[index].WidthKM-linear) > occultationPathWidthToleranceKM { withinTolerance = false } } anchors := append([]int{left}, indices...) anchors = append(anchors, right) if withinTolerance { for index := 1; index < len(anchors); index++ { interpolateOccultationPathWidths(points, anchors[index-1], anchors[index]) } return } for index := 1; index < len(anchors); index++ { refine(anchors[index-1], anchors[index], depth+1) } } refine(0, len(points)-1, 0) } func uniqueOccultationPathWidthIndices(left, right int) []int { indices := make([]int, 0, 3) for _, numerator := range []int{1, 2, 3} { index := left + (right-left)*numerator/4 if index <= left || index >= right || len(indices) > 0 && index == indices[len(indices)-1] { continue } indices = append(indices, index) } return indices } func setOccultationPathExactWidth( points []OccultationPathPoint, index int, widthAt occultationPathWidthFunc, cache map[int]bool, ) bool { if ok, found := cache[index]; found { return ok } width, ok := widthAt(centerTimeTT(points[index].Time)) if ok && finite(width) && width >= 0 { points[index].WidthKM = width } else { ok = false } cache[index] = ok return ok } func interpolateOccultationPathWidths(points []OccultationPathPoint, left, right int) { if right-left <= 1 { return } leftTime := points[left].Time duration := points[right].Time.Sub(leftTime).Seconds() for index := left + 1; index < right; index++ { fraction := float64(index-left) / float64(right-left) if duration != 0 { fraction = points[index].Time.Sub(leftTime).Seconds() / duration } points[index].WidthKM = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM) } } func occultationPathDistanceKM(a, b OccultationPathPoint) float64 { return occultationPathDistanceKMValues(a.Longitude, a.Latitude, b.Longitude, b.Latitude) } func occultationPathDistanceKMValues(lon1, lat1, lon2, lat2 float64) float64 { lat1 *= math.Pi / 180 lat2 *= math.Pi / 180 dLat := lat2 - lat1 dLon := (lon2 - lon1) * math.Pi / 180 dLon = math.Mod(dLon+math.Pi, 2*math.Pi) if dLon < 0 { dLon += 2 * math.Pi } dLon -= math.Pi h := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2) return 2 * occultationPathEarthEquatorialRadiusKM * math.Asin(math.Sqrt(math.Min(1, h))) } type occultationPathVector struct{ x, y, z float64 } func occultationPathRaDecVector(ra, dec, distance float64) occultationPathVector { ra *= math.Pi / 180 dec *= math.Pi / 180 return occultationPathVector{ x: distance * math.Cos(dec) * math.Cos(ra), y: distance * math.Cos(dec) * math.Sin(ra), z: distance * math.Sin(dec), } } func occultationPathAdd(a, b occultationPathVector) occultationPathVector { return occultationPathVector{x: a.x + b.x, y: a.y + b.y, z: a.z + b.z} } func occultationPathSub(a, b occultationPathVector) occultationPathVector { return occultationPathVector{x: a.x - b.x, y: a.y - b.y, z: a.z - b.z} } func occultationPathScale(a occultationPathVector, scalar float64) occultationPathVector { return occultationPathVector{x: a.x * scalar, y: a.y * scalar, z: a.z * scalar} } func occultationPathDot(a, b occultationPathVector) float64 { return a.x*b.x + a.y*b.y + a.z*b.z } func occultationPathCross(a, b occultationPathVector) occultationPathVector { return occultationPathVector{ x: a.y*b.z - a.z*b.y, y: a.z*b.x - a.x*b.z, z: a.x*b.y - a.y*b.x, } } func occultationPathNorm(value occultationPathVector) float64 { return math.Sqrt(occultationPathDot(value, value)) } func occultationPathUnit(value occultationPathVector) occultationPathVector { norm := occultationPathNorm(value) if norm <= 0 { return occultationPathVector{} } return occultationPathScale(value, 1/norm) } func occultationEarthLineIntersection(origin, direction occultationPathVector) (occultationPathVector, float64, bool) { return occultationEarthLineIntersectionWithTolerance(origin, direction, 0) } // occultationPathTrackReference 将影轴地面轨迹连续延伸到掠过阶段 / // occultationPathTrackReference extends the shadow-axis ground track through grazing phases. // 在地球外时,将椭球度量下最近点径向投影到表面,并在相切处与真实近侧交点连续连接 / // Outside the Earth, the closest point under the ellipsoid metric is projected radially onto the surface and joined continuously to the real near-side intersection at tangency. func occultationPathTrackReference(frame occultationPathFrame) (occultationPathVector, bool) { if point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis); ok { return point, true } polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio a := frame.axis.x*frame.axis.x + frame.axis.y*frame.axis.y + frame.axis.z*frame.axis.z/polarRatioSquared b := frame.moon.x*frame.axis.x + frame.moon.y*frame.axis.y + frame.moon.z*frame.axis.z/polarRatioSquared if a <= 0 || !finite(a) || !finite(b) { return occultationPathVector{}, false } closest := occultationPathAdd(frame.moon, occultationPathScale(frame.axis, -b/a)) metricRadius := math.Sqrt(closest.x*closest.x + closest.y*closest.y + closest.z*closest.z/polarRatioSquared) if metricRadius <= 1e-9 || !finite(metricRadius) { return occultationPathVector{}, false } return occultationPathScale(closest, occultationPathEarthEquatorialRadiusKM/metricRadius), true } func occultationEarthLineIntersectionWithTolerance(origin, direction occultationPathVector, tolerance float64) (occultationPathVector, float64, bool) { polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared b := origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM discriminant := b*b - a*c if discriminant < -tolerance || a <= 0 { return occultationPathVector{}, 0, false } if discriminant < 0 { discriminant = 0 } root := math.Sqrt(discriminant) roots := [2]float64{(-b - root) / a, (-b + root) / a} chosen := math.Inf(1) for _, root := range roots { if root >= 0 && root < chosen { chosen = root } } if math.IsInf(chosen, 1) { return occultationPathVector{}, 0, false } return occultationPathAdd(origin, occultationPathScale(direction, chosen)), chosen, true } func occultationPathGeodetic(tt float64, vector occultationPathVector) (float64, float64) { ut := TD2UT(tt, false) gst := ApparentSiderealTime(ut) * 15 longitude := normalizeLongitude(math.Atan2(vector.y, vector.x)*180/math.Pi - gst) latitude := math.Atan2( vector.z, occultationPathEarthPolarRatio*occultationPathEarthPolarRatio*math.Hypot(vector.x, vector.y), ) * 180 / math.Pi return longitude, latitude } func occultationPathEarthFixedVector(tt float64, vector occultationPathVector) occultationPathVector { angle := ApparentSiderealTime(TD2UT(tt, false)) * 15 * math.Pi / 180 cosAngle := math.Cos(angle) sinAngle := math.Sin(angle) return occultationPathVector{ x: cosAngle*vector.x + sinAngle*vector.y, y: -sinAngle*vector.x + cosAngle*vector.y, z: vector.z, } } func normalizeLongitude(longitude float64) float64 { longitude = math.Mod(longitude+180, 360) if longitude < 0 { longitude += 360 } return longitude - 180 }