681 lines
25 KiB
Go
681 lines
25 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
solarEclipsePathDefaultStepDays = 1.0 / 1440.0
|
||
|
|
solarEclipsePathMinStepDays = 1.0 / 86400.0
|
||
|
|
solarEclipsePathMaxSampleCount = 30000
|
||
|
|
solarEclipsePathMaxAdaptiveDepth = 20
|
||
|
|
solarEclipsePathVelocityStepDays = 1.0 / 1440.0
|
||
|
|
solarEclipsePathDuplicateTimeDays = 1e-10
|
||
|
|
|
||
|
|
solarEclipsePartialFootprintDefaultStepDays = 5.0 / 1440.0
|
||
|
|
solarEclipsePartialFootprintDefaultBoundaryPoints = 180
|
||
|
|
solarEclipsePartialFootprintMinBoundaryPoints = 12
|
||
|
|
solarEclipsePartialFootprintMaxBoundaryPoints = 1440
|
||
|
|
solarEclipsePartialFootprintPointTolerance = 1e-12
|
||
|
|
solarEclipsePartialFootprintIterationLimit = 10
|
||
|
|
)
|
||
|
|
|
||
|
|
// SolarEclipsePathOptions 控制日食中心路径采样。
|
||
|
|
// SolarEclipsePathOptions controls central solar eclipse path sampling.
|
||
|
|
type SolarEclipsePathOptions struct {
|
||
|
|
// StepDays 是基础时间采样步长,单位为日;<=0 时使用 1 分钟。
|
||
|
|
// StepDays is the base time step in days; values <= 0 use one minute.
|
||
|
|
StepDays float64
|
||
|
|
// TargetSpacingKM 是相邻中心线点的最大目标地表距离;<=0 时不按距离加密。
|
||
|
|
// TargetSpacingKM is the target maximum ground spacing between centerline points; values <= 0 disable spacing refinement.
|
||
|
|
TargetSpacingKM float64
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePathPoint 表示日食路径上的一个地理点。
|
||
|
|
// SolarEclipsePathPoint is one geographic point on a solar eclipse path.
|
||
|
|
type SolarEclipsePathPoint struct {
|
||
|
|
// JDE 是力学时儒略日, TT Julian ephemeris day.
|
||
|
|
JDE float64
|
||
|
|
// Longitude 经度,东正西负, longitude in degrees, east positive.
|
||
|
|
Longitude float64
|
||
|
|
// Latitude 纬度,北正南负, latitude in degrees, north positive.
|
||
|
|
Latitude float64
|
||
|
|
// SunAltitude 太阳高度角,单位度, Sun altitude in degrees.
|
||
|
|
SunAltitude float64
|
||
|
|
// WidthKM 中心食带宽度,单位千米;仅中心线点有意义。
|
||
|
|
// WidthKM is the central path width in kilometers; meaningful for centerline points.
|
||
|
|
WidthKM float64
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePathResult 表示一次中心日食的路径数据。
|
||
|
|
// SolarEclipsePathResult contains central solar eclipse path data.
|
||
|
|
type SolarEclipsePathResult struct {
|
||
|
|
// Eclipse 是对应的全局日食结果, related global solar eclipse result.
|
||
|
|
Eclipse SolarEclipseResult
|
||
|
|
// Greatest 是食甚点/最佳观测点, greatest eclipse point.
|
||
|
|
Greatest SolarEclipsePathPoint
|
||
|
|
// CenterLine 是中心线, central line.
|
||
|
|
CenterLine []SolarEclipsePathPoint
|
||
|
|
// NorthernLimit 是中心食带北界近似线, approximate northern limit of the central path.
|
||
|
|
NorthernLimit []SolarEclipsePathPoint
|
||
|
|
// SouthernLimit 是中心食带南界近似线, approximate southern limit of the central path.
|
||
|
|
SouthernLimit []SolarEclipsePathPoint
|
||
|
|
// StepDays 是实际采用的基础时间采样步长,单位为日。
|
||
|
|
// StepDays is the effective base time step in days.
|
||
|
|
StepDays float64
|
||
|
|
// TargetSpacingKM 是实际采用的目标空间采样距离,单位千米。
|
||
|
|
// TargetSpacingKM is the effective target spacing in kilometers.
|
||
|
|
TargetSpacingKM float64
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialFootprintOptions 控制日食偏食半影足迹采样。
|
||
|
|
// SolarEclipsePartialFootprintOptions controls solar eclipse penumbral footprint sampling.
|
||
|
|
type SolarEclipsePartialFootprintOptions struct {
|
||
|
|
// StepDays 是基础时间采样步长,单位为日;<=0 时使用 5 分钟。
|
||
|
|
// StepDays is the base time step in days; values <= 0 use five minutes.
|
||
|
|
StepDays float64
|
||
|
|
// BoundaryPoints 是每个瞬时半影边界的角向采样点数;<=0 时使用 180。
|
||
|
|
// BoundaryPoints is the angular sample count for each instantaneous penumbral boundary; values <= 0 use 180.
|
||
|
|
BoundaryPoints int
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialAreaOptions 是 SolarEclipsePartialFootprintOptions 的兼容别名。
|
||
|
|
// SolarEclipsePartialAreaOptions is a compatibility alias for SolarEclipsePartialFootprintOptions.
|
||
|
|
type SolarEclipsePartialAreaOptions = SolarEclipsePartialFootprintOptions
|
||
|
|
|
||
|
|
// SolarEclipsePartialFootprint 表示某一时刻的半影足迹边界。
|
||
|
|
// SolarEclipsePartialFootprint is the penumbral footprint boundary at one instant.
|
||
|
|
type SolarEclipsePartialFootprint struct {
|
||
|
|
// JDE 是力学时儒略日, TT Julian ephemeris day.
|
||
|
|
JDE float64
|
||
|
|
// Boundaries 是半影边界分段;反经线或无效投影会拆成多段。
|
||
|
|
// Boundaries are segmented penumbral boundary polylines, split at invalid projections or the antimeridian.
|
||
|
|
Boundaries [][]SolarEclipsePathPoint
|
||
|
|
// Closed 表示 Boundaries 是否构成一个闭合边界。
|
||
|
|
// Closed indicates whether Boundaries form one closed boundary.
|
||
|
|
Closed bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialFootprintsResult 表示一次日食的偏食半影足迹序列。
|
||
|
|
// SolarEclipsePartialFootprintsResult contains penumbral footprint samples for a solar eclipse.
|
||
|
|
type SolarEclipsePartialFootprintsResult struct {
|
||
|
|
// Eclipse 是对应的全局日食结果, related global solar eclipse result.
|
||
|
|
Eclipse SolarEclipseResult
|
||
|
|
// Footprints 是按时间采样的瞬时半影足迹, sampled instantaneous penumbral footprints.
|
||
|
|
Footprints []SolarEclipsePartialFootprint
|
||
|
|
// StepDays 是实际采用的基础时间采样步长,单位为日。
|
||
|
|
// StepDays is the effective base time step in days.
|
||
|
|
StepDays float64
|
||
|
|
// BoundaryPoints 是实际采用的边界角向采样点数。
|
||
|
|
// BoundaryPoints is the effective angular sample count for each boundary.
|
||
|
|
BoundaryPoints int
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialAreaResult 是 SolarEclipsePartialFootprintsResult 的兼容别名。
|
||
|
|
// SolarEclipsePartialAreaResult is a compatibility alias for SolarEclipsePartialFootprintsResult.
|
||
|
|
type SolarEclipsePartialAreaResult = SolarEclipsePartialFootprintsResult
|
||
|
|
|
||
|
|
// SolarEclipseCentralPath 计算给定近朔时刻附近的日食中心路径,默认使用 NASA bulletin Split-K 模型。
|
||
|
|
// SolarEclipseCentralPath computes the central path near the given new-moon seed, using NASA bulletin Split-K by default.
|
||
|
|
func SolarEclipseCentralPath(seedJDE float64, options SolarEclipsePathOptions) SolarEclipsePathResult {
|
||
|
|
return SolarEclipseCentralPathNASABulletinSplitK(seedJDE, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipseCentralPathIAUSingleK 计算日食中心路径,使用 IAU Single-K 模型。
|
||
|
|
// SolarEclipseCentralPathIAUSingleK computes the central path with the IAU Single-K model.
|
||
|
|
func SolarEclipseCentralPathIAUSingleK(seedJDE float64, options SolarEclipsePathOptions) SolarEclipsePathResult {
|
||
|
|
return solarEclipseCentralPath(seedJDE, SolarEclipseModelIAUSingleK, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipseCentralPathNASABulletinSplitK 计算日食中心路径,使用 NASA bulletin Split-K 模型。
|
||
|
|
// SolarEclipseCentralPathNASABulletinSplitK computes the central path with the NASA bulletin Split-K model.
|
||
|
|
func SolarEclipseCentralPathNASABulletinSplitK(seedJDE float64, options SolarEclipsePathOptions) SolarEclipsePathResult {
|
||
|
|
return solarEclipseCentralPath(seedJDE, SolarEclipseModelNASABulletinSplitK, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialFootprints 计算给定近朔时刻附近的日食偏食半影足迹序列,默认使用 NASA bulletin Split-K 模型。
|
||
|
|
// SolarEclipsePartialFootprints computes penumbral footprint samples near the given new-moon seed, using NASA bulletin Split-K by default.
|
||
|
|
func SolarEclipsePartialFootprints(seedJDE float64, options SolarEclipsePartialFootprintOptions) SolarEclipsePartialFootprintsResult {
|
||
|
|
return SolarEclipsePartialFootprintsNASABulletinSplitK(seedJDE, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialFootprintsIAUSingleK 计算日食偏食半影足迹序列,使用 IAU Single-K 模型。
|
||
|
|
// SolarEclipsePartialFootprintsIAUSingleK computes penumbral footprint samples with the IAU Single-K model.
|
||
|
|
func SolarEclipsePartialFootprintsIAUSingleK(seedJDE float64, options SolarEclipsePartialFootprintOptions) SolarEclipsePartialFootprintsResult {
|
||
|
|
return solarEclipsePartialFootprints(seedJDE, SolarEclipseModelIAUSingleK, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialFootprintsNASABulletinSplitK 计算日食偏食半影足迹序列,使用 NASA bulletin Split-K 模型。
|
||
|
|
// SolarEclipsePartialFootprintsNASABulletinSplitK computes penumbral footprint samples with the NASA bulletin Split-K model.
|
||
|
|
func SolarEclipsePartialFootprintsNASABulletinSplitK(seedJDE float64, options SolarEclipsePartialFootprintOptions) SolarEclipsePartialFootprintsResult {
|
||
|
|
return solarEclipsePartialFootprints(seedJDE, SolarEclipseModelNASABulletinSplitK, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialArea 计算日食偏食半影足迹序列,是 SolarEclipsePartialFootprints 的兼容包装。
|
||
|
|
// SolarEclipsePartialArea computes penumbral footprint samples and is a compatibility wrapper for SolarEclipsePartialFootprints.
|
||
|
|
func SolarEclipsePartialArea(seedJDE float64, options SolarEclipsePartialAreaOptions) SolarEclipsePartialAreaResult {
|
||
|
|
return SolarEclipsePartialFootprints(seedJDE, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialAreaIAUSingleK 计算日食偏食半影足迹序列,是 SolarEclipsePartialFootprintsIAUSingleK 的兼容包装。
|
||
|
|
// SolarEclipsePartialAreaIAUSingleK is a compatibility wrapper for SolarEclipsePartialFootprintsIAUSingleK.
|
||
|
|
func SolarEclipsePartialAreaIAUSingleK(seedJDE float64, options SolarEclipsePartialAreaOptions) SolarEclipsePartialAreaResult {
|
||
|
|
return SolarEclipsePartialFootprintsIAUSingleK(seedJDE, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SolarEclipsePartialAreaNASABulletinSplitK 计算日食偏食半影足迹序列,是 SolarEclipsePartialFootprintsNASABulletinSplitK 的兼容包装。
|
||
|
|
// SolarEclipsePartialAreaNASABulletinSplitK is a compatibility wrapper for SolarEclipsePartialFootprintsNASABulletinSplitK.
|
||
|
|
func SolarEclipsePartialAreaNASABulletinSplitK(seedJDE float64, options SolarEclipsePartialAreaOptions) SolarEclipsePartialAreaResult {
|
||
|
|
return SolarEclipsePartialFootprintsNASABulletinSplitK(seedJDE, options)
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseCentralPath(seedJDE float64, model SolarEclipseRadiusModel, options SolarEclipsePathOptions) SolarEclipsePathResult {
|
||
|
|
options = normalizeSolarEclipsePathOptions(options)
|
||
|
|
result := solarEclipse(seedJDE, model)
|
||
|
|
path := SolarEclipsePathResult{
|
||
|
|
Eclipse: result,
|
||
|
|
StepDays: options.StepDays,
|
||
|
|
TargetSpacingKM: options.TargetSpacingKM,
|
||
|
|
}
|
||
|
|
if !result.HasCentral {
|
||
|
|
return path
|
||
|
|
}
|
||
|
|
|
||
|
|
newMoonJDE := CalcMoonSHByJDE(seedJDE, 0)
|
||
|
|
solver := newSolarEclipseSolver(newMoonJDE, model)
|
||
|
|
greatest, ok := solver.centralPathPointAt(result.GreatestEclipse)
|
||
|
|
if !ok {
|
||
|
|
greatest = SolarEclipsePathPoint{
|
||
|
|
JDE: result.GreatestEclipse,
|
||
|
|
Longitude: result.GreatestLongitude,
|
||
|
|
Latitude: result.GreatestLatitude,
|
||
|
|
WidthKM: result.PathWidthKM,
|
||
|
|
SunAltitude: solarEclipseSunAltitudeAtGreatest(
|
||
|
|
result.GreatestEclipse,
|
||
|
|
result.GreatestLongitude,
|
||
|
|
result.GreatestLatitude,
|
||
|
|
solver.besselAxisAt(result.GreatestEclipse).gst,
|
||
|
|
) / rad,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
greatest.Longitude = result.GreatestLongitude
|
||
|
|
greatest.Latitude = result.GreatestLatitude
|
||
|
|
greatest.WidthKM = result.PathWidthKM
|
||
|
|
path.Greatest = greatest
|
||
|
|
|
||
|
|
centerLine, stepDays := solver.centralPathPoints(
|
||
|
|
result.CentralBeginOnEarth,
|
||
|
|
result.CentralEndOnEarth,
|
||
|
|
result.GreatestEclipse,
|
||
|
|
options,
|
||
|
|
)
|
||
|
|
path.StepDays = stepDays
|
||
|
|
path.CenterLine = centerLine
|
||
|
|
path.NorthernLimit, path.SouthernLimit = solver.centralPathLimits(centerLine)
|
||
|
|
return path
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeSolarEclipsePathOptions(options SolarEclipsePathOptions) SolarEclipsePathOptions {
|
||
|
|
if options.StepDays <= 0 || math.IsNaN(options.StepDays) || math.IsInf(options.StepDays, 0) {
|
||
|
|
options.StepDays = solarEclipsePathDefaultStepDays
|
||
|
|
}
|
||
|
|
if options.StepDays < solarEclipsePathMinStepDays {
|
||
|
|
options.StepDays = solarEclipsePathMinStepDays
|
||
|
|
}
|
||
|
|
if options.TargetSpacingKM <= 0 || math.IsNaN(options.TargetSpacingKM) || math.IsInf(options.TargetSpacingKM, 0) {
|
||
|
|
options.TargetSpacingKM = 0
|
||
|
|
}
|
||
|
|
return options
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipsePartialFootprints(
|
||
|
|
seedJDE float64,
|
||
|
|
model SolarEclipseRadiusModel,
|
||
|
|
options SolarEclipsePartialFootprintOptions,
|
||
|
|
) SolarEclipsePartialFootprintsResult {
|
||
|
|
options = normalizeSolarEclipsePartialFootprintOptions(options)
|
||
|
|
result := solarEclipse(seedJDE, model)
|
||
|
|
footprintsResult := SolarEclipsePartialFootprintsResult{
|
||
|
|
Eclipse: result,
|
||
|
|
StepDays: options.StepDays,
|
||
|
|
BoundaryPoints: options.BoundaryPoints,
|
||
|
|
}
|
||
|
|
if !result.HasPartial {
|
||
|
|
return footprintsResult
|
||
|
|
}
|
||
|
|
|
||
|
|
newMoonJDE := CalcMoonSHByJDE(seedJDE, 0)
|
||
|
|
solver := newSolarEclipseSolver(newMoonJDE, model)
|
||
|
|
footprints, stepDays := solver.partialFootprints(
|
||
|
|
result.PartialBeginOnEarth,
|
||
|
|
result.PartialEndOnEarth,
|
||
|
|
result.GreatestEclipse,
|
||
|
|
options,
|
||
|
|
)
|
||
|
|
footprintsResult.StepDays = stepDays
|
||
|
|
footprintsResult.Footprints = footprints
|
||
|
|
return footprintsResult
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeSolarEclipsePartialFootprintOptions(options SolarEclipsePartialFootprintOptions) SolarEclipsePartialFootprintOptions {
|
||
|
|
if options.StepDays <= 0 || math.IsNaN(options.StepDays) || math.IsInf(options.StepDays, 0) {
|
||
|
|
options.StepDays = solarEclipsePartialFootprintDefaultStepDays
|
||
|
|
}
|
||
|
|
if options.StepDays < solarEclipsePathMinStepDays {
|
||
|
|
options.StepDays = solarEclipsePathMinStepDays
|
||
|
|
}
|
||
|
|
if options.BoundaryPoints <= 0 {
|
||
|
|
options.BoundaryPoints = solarEclipsePartialFootprintDefaultBoundaryPoints
|
||
|
|
}
|
||
|
|
if options.BoundaryPoints < solarEclipsePartialFootprintMinBoundaryPoints {
|
||
|
|
options.BoundaryPoints = solarEclipsePartialFootprintMinBoundaryPoints
|
||
|
|
}
|
||
|
|
if options.BoundaryPoints > solarEclipsePartialFootprintMaxBoundaryPoints {
|
||
|
|
options.BoundaryPoints = solarEclipsePartialFootprintMaxBoundaryPoints
|
||
|
|
}
|
||
|
|
return options
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralPathPoints(
|
||
|
|
startJDE, endJDE, greatestJDE float64,
|
||
|
|
options SolarEclipsePathOptions,
|
||
|
|
) ([]SolarEclipsePathPoint, float64) {
|
||
|
|
if endJDE < startJDE {
|
||
|
|
startJDE, endJDE = endJDE, startJDE
|
||
|
|
}
|
||
|
|
if startJDE == 0 || endJDE == 0 || endJDE <= startJDE {
|
||
|
|
return nil, options.StepDays
|
||
|
|
}
|
||
|
|
|
||
|
|
stepDays := options.StepDays
|
||
|
|
if sampleCount := int(math.Ceil((endJDE-startJDE)/stepDays)) + 1; sampleCount > solarEclipsePathMaxSampleCount {
|
||
|
|
stepDays = (endJDE - startJDE) / float64(solarEclipsePathMaxSampleCount-1)
|
||
|
|
}
|
||
|
|
|
||
|
|
times := []float64{startJDE, greatestJDE, endJDE}
|
||
|
|
for jd := startJDE + stepDays; jd < endJDE; jd += stepDays {
|
||
|
|
times = append(times, jd)
|
||
|
|
}
|
||
|
|
sort.Float64s(times)
|
||
|
|
times = uniqueSolarEclipsePathTimes(times)
|
||
|
|
|
||
|
|
points := make([]SolarEclipsePathPoint, 0, len(times))
|
||
|
|
for _, jd := range times {
|
||
|
|
point, ok := solver.centralPathPointAt(jd)
|
||
|
|
if ok {
|
||
|
|
points = append(points, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if options.TargetSpacingKM > 0 {
|
||
|
|
points = solver.refineCentralPathSpacing(points, options.TargetSpacingKM)
|
||
|
|
}
|
||
|
|
return points, stepDays
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) partialFootprints(
|
||
|
|
startJDE, endJDE, greatestJDE float64,
|
||
|
|
options SolarEclipsePartialFootprintOptions,
|
||
|
|
) ([]SolarEclipsePartialFootprint, float64) {
|
||
|
|
if endJDE < startJDE {
|
||
|
|
startJDE, endJDE = endJDE, startJDE
|
||
|
|
}
|
||
|
|
if startJDE == 0 || endJDE == 0 || endJDE <= startJDE {
|
||
|
|
return nil, options.StepDays
|
||
|
|
}
|
||
|
|
|
||
|
|
stepDays := options.StepDays
|
||
|
|
if sampleCount := int(math.Ceil((endJDE-startJDE)/stepDays)) + 1; sampleCount > solarEclipsePathMaxSampleCount {
|
||
|
|
stepDays = (endJDE - startJDE) / float64(solarEclipsePathMaxSampleCount-1)
|
||
|
|
}
|
||
|
|
|
||
|
|
times := []float64{startJDE, greatestJDE, endJDE}
|
||
|
|
for jd := startJDE + stepDays; jd < endJDE; jd += stepDays {
|
||
|
|
times = append(times, jd)
|
||
|
|
}
|
||
|
|
sort.Float64s(times)
|
||
|
|
times = uniqueSolarEclipsePathTimes(times)
|
||
|
|
|
||
|
|
footprints := make([]SolarEclipsePartialFootprint, 0, len(times))
|
||
|
|
for _, jd := range times {
|
||
|
|
footprint := solver.partialFootprintAt(jd, options.BoundaryPoints)
|
||
|
|
if len(footprint.Boundaries) > 0 {
|
||
|
|
footprints = append(footprints, footprint)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return footprints, stepDays
|
||
|
|
}
|
||
|
|
|
||
|
|
func uniqueSolarEclipsePathTimes(times []float64) []float64 {
|
||
|
|
if len(times) < 2 {
|
||
|
|
return times
|
||
|
|
}
|
||
|
|
|
||
|
|
unique := times[:1]
|
||
|
|
for _, jd := range times[1:] {
|
||
|
|
if math.Abs(jd-unique[len(unique)-1]) <= solarEclipsePathDuplicateTimeDays {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
unique = append(unique, jd)
|
||
|
|
}
|
||
|
|
return unique
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) refineCentralPathSpacing(points []SolarEclipsePathPoint, targetSpacingKM float64) []SolarEclipsePathPoint {
|
||
|
|
if len(points) < 2 || targetSpacingKM <= 0 {
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
|
||
|
|
refined := make([]SolarEclipsePathPoint, 0, len(points))
|
||
|
|
refined = append(refined, points[0])
|
||
|
|
for i := 1; i < len(points); i++ {
|
||
|
|
refined = solver.appendRefinedCentralPathSegment(refined, points[i-1], points[i], targetSpacingKM, 0)
|
||
|
|
}
|
||
|
|
return refined
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) appendRefinedCentralPathSegment(
|
||
|
|
points []SolarEclipsePathPoint,
|
||
|
|
start, end SolarEclipsePathPoint,
|
||
|
|
targetSpacingKM float64,
|
||
|
|
depth int,
|
||
|
|
) []SolarEclipsePathPoint {
|
||
|
|
if depth >= solarEclipsePathMaxAdaptiveDepth || solarEclipsePathDistanceKM(start, end) <= targetSpacingKM {
|
||
|
|
return append(points, end)
|
||
|
|
}
|
||
|
|
|
||
|
|
midJDE := (start.JDE + end.JDE) / 2
|
||
|
|
mid, ok := solver.centralPathPointAt(midJDE)
|
||
|
|
if !ok {
|
||
|
|
return append(points, end)
|
||
|
|
}
|
||
|
|
points = solver.appendRefinedCentralPathSegment(points, start, mid, targetSpacingKM, depth+1)
|
||
|
|
return solver.appendRefinedCentralPathSegment(points, mid, end, targetSpacingKM, depth+1)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralPathPointAt(jd float64) (SolarEclipsePathPoint, bool) {
|
||
|
|
moon := solver.besselMoonAt(jd)
|
||
|
|
axis := solver.besselAxisAt(jd)
|
||
|
|
intersection := solarEclipseLineEar2(
|
||
|
|
moon[0],
|
||
|
|
moon[1],
|
||
|
|
2,
|
||
|
|
moon[0],
|
||
|
|
moon[1],
|
||
|
|
0,
|
||
|
|
solarEclipseEarthPolarRatio,
|
||
|
|
1,
|
||
|
|
axis,
|
||
|
|
)
|
||
|
|
if !intersection.valid {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
longitude, latitude := solarEclipseIntersectionGeodetic(intersection, axis)
|
||
|
|
sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jd, longitude, latitude, axis.gst)
|
||
|
|
radii := solver.shadowRadiiAt(moon[2] - intersection.r2)
|
||
|
|
|
||
|
|
widthKM := 0.0
|
||
|
|
if math.Abs(math.Sin(sunAltitudeRad)) > 1e-12 {
|
||
|
|
widthKM = math.Abs(2*radii.umbraRadius*solarEclipseEarthEquatorialRadiusKM) / math.Abs(math.Sin(sunAltitudeRad))
|
||
|
|
}
|
||
|
|
|
||
|
|
return SolarEclipsePathPoint{
|
||
|
|
JDE: jd,
|
||
|
|
Longitude: longitude,
|
||
|
|
Latitude: latitude,
|
||
|
|
SunAltitude: sunAltitudeRad / rad,
|
||
|
|
WidthKM: widthKM,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralPathLimits(centerLine []SolarEclipsePathPoint) ([]SolarEclipsePathPoint, []SolarEclipsePathPoint) {
|
||
|
|
northern := make([]SolarEclipsePathPoint, 0, len(centerLine))
|
||
|
|
southern := make([]SolarEclipsePathPoint, 0, len(centerLine))
|
||
|
|
for _, center := range centerLine {
|
||
|
|
north, south, ok := solver.centralPathLimitsAt(center)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
northern = append(northern, north)
|
||
|
|
southern = append(southern, south)
|
||
|
|
}
|
||
|
|
return northern, southern
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralPathLimitsAt(center SolarEclipsePathPoint) (SolarEclipsePathPoint, SolarEclipsePathPoint, bool) {
|
||
|
|
moon := solver.besselMoonAt(center.JDE)
|
||
|
|
axis := solver.besselAxisAt(center.JDE)
|
||
|
|
intersection := solarEclipseLineEar2(
|
||
|
|
moon[0],
|
||
|
|
moon[1],
|
||
|
|
2,
|
||
|
|
moon[0],
|
||
|
|
moon[1],
|
||
|
|
0,
|
||
|
|
solarEclipseEarthPolarRatio,
|
||
|
|
1,
|
||
|
|
axis,
|
||
|
|
)
|
||
|
|
if !intersection.valid {
|
||
|
|
return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
radii := solver.shadowRadiiAt(moon[2] - intersection.r2)
|
||
|
|
radius := radii.absUmbraRadius
|
||
|
|
if radius <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
vx, vy, speed := solver.besselVelocityXYAt(center.JDE)
|
||
|
|
if speed <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
perpX := -vy / speed
|
||
|
|
perpY := vx / speed
|
||
|
|
|
||
|
|
first, okFirst := solarEclipsePathPointFromBesselXY(center.JDE, moon[0]+radius*perpX, moon[1]+radius*perpY, axis)
|
||
|
|
second, okSecond := solarEclipsePathPointFromBesselXY(center.JDE, moon[0]-radius*perpX, moon[1]-radius*perpY, axis)
|
||
|
|
if !okFirst || !okSecond {
|
||
|
|
return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
first.WidthKM = center.WidthKM
|
||
|
|
second.WidthKM = center.WidthKM
|
||
|
|
if first.Latitude >= second.Latitude {
|
||
|
|
return first, second, true
|
||
|
|
}
|
||
|
|
return second, first, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) besselVelocityXYAt(jd float64) (float64, float64, float64) {
|
||
|
|
before := solver.besselMoonAt(jd - solarEclipsePathVelocityStepDays)
|
||
|
|
after := solver.besselMoonAt(jd + solarEclipsePathVelocityStepDays)
|
||
|
|
vx := (after[0] - before[0]) / (2 * solarEclipsePathVelocityStepDays)
|
||
|
|
vy := (after[1] - before[1]) / (2 * solarEclipsePathVelocityStepDays)
|
||
|
|
return vx, vy, math.Hypot(vx, vy)
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipsePathPointFromBesselXY(jd, x, y float64, axis solarEclipseAxis) (SolarEclipsePathPoint, bool) {
|
||
|
|
longitude, latitude, ok := solarEclipseBesselXYToGeodetic(x, y, axis, true)
|
||
|
|
if !ok {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jd, longitude, latitude, axis.gst)
|
||
|
|
return SolarEclipsePathPoint{
|
||
|
|
JDE: jd,
|
||
|
|
Longitude: longitude,
|
||
|
|
Latitude: latitude,
|
||
|
|
SunAltitude: sunAltitudeRad / rad,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) partialFootprintAt(jd float64, boundaryPoints int) SolarEclipsePartialFootprint {
|
||
|
|
moon := solver.besselMoonAt(jd)
|
||
|
|
axis := solver.besselAxisAt(jd)
|
||
|
|
samples := make([]solarEclipsePartialBoundarySample, boundaryPoints)
|
||
|
|
for i := range samples {
|
||
|
|
angle := 2 * math.Pi * float64(i) / float64(boundaryPoints)
|
||
|
|
point, ok := solver.partialFootprintPointAt(jd, moon, axis, angle)
|
||
|
|
samples[i] = solarEclipsePartialBoundarySample{
|
||
|
|
point: point,
|
||
|
|
ok: ok,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
boundaries, closed := solarEclipsePartialBoundarySegments(samples)
|
||
|
|
return SolarEclipsePartialFootprint{
|
||
|
|
JDE: jd,
|
||
|
|
Boundaries: boundaries,
|
||
|
|
Closed: closed,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type solarEclipsePartialBoundarySample struct {
|
||
|
|
point SolarEclipsePathPoint
|
||
|
|
ok bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) partialFootprintPointAt(
|
||
|
|
jd float64,
|
||
|
|
moon [3]float64,
|
||
|
|
axis solarEclipseAxis,
|
||
|
|
angle float64,
|
||
|
|
) (SolarEclipsePathPoint, bool) {
|
||
|
|
cosAngle := math.Cos(angle)
|
||
|
|
sinAngle := math.Sin(angle)
|
||
|
|
radius := solver.shadowRadiiAt(moon[2]).penumbraRadius
|
||
|
|
if radius <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
var intersection solarEclipseLineIntersection
|
||
|
|
for i := 0; i < solarEclipsePartialFootprintIterationLimit; i++ {
|
||
|
|
x := moon[0] + radius*cosAngle
|
||
|
|
y := moon[1] + radius*sinAngle
|
||
|
|
intersection = solarEclipseLineEar2(
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
2,
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
0,
|
||
|
|
solarEclipseEarthPolarRatio,
|
||
|
|
1,
|
||
|
|
axis,
|
||
|
|
)
|
||
|
|
if !intersection.valid {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
nextRadius := solver.shadowRadiiAt(moon[2] - intersection.r2).penumbraRadius
|
||
|
|
if nextRadius <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
if math.Abs(nextRadius-radius) <= solarEclipsePartialFootprintPointTolerance {
|
||
|
|
radius = nextRadius
|
||
|
|
break
|
||
|
|
}
|
||
|
|
radius = nextRadius
|
||
|
|
}
|
||
|
|
|
||
|
|
x := moon[0] + radius*cosAngle
|
||
|
|
y := moon[1] + radius*sinAngle
|
||
|
|
intersection = solarEclipseLineEar2(
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
2,
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
0,
|
||
|
|
solarEclipseEarthPolarRatio,
|
||
|
|
1,
|
||
|
|
axis,
|
||
|
|
)
|
||
|
|
if !intersection.valid {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
longitude, latitude := solarEclipseIntersectionGeodetic(intersection, axis)
|
||
|
|
sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jd, longitude, latitude, axis.gst)
|
||
|
|
return SolarEclipsePathPoint{
|
||
|
|
JDE: jd,
|
||
|
|
Longitude: longitude,
|
||
|
|
Latitude: latitude,
|
||
|
|
SunAltitude: sunAltitudeRad / rad,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipsePartialBoundarySegments(samples []solarEclipsePartialBoundarySample) ([][]SolarEclipsePathPoint, bool) {
|
||
|
|
segments := make([][]SolarEclipsePathPoint, 0, 2)
|
||
|
|
var current []SolarEclipsePathPoint
|
||
|
|
for _, sample := range samples {
|
||
|
|
if !sample.ok {
|
||
|
|
segments = appendSolarEclipsePartialSegment(segments, current)
|
||
|
|
current = nil
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if len(current) > 0 && solarEclipsePathCrossesAntimeridian(current[len(current)-1], sample.point) {
|
||
|
|
segments = appendSolarEclipsePartialSegment(segments, current)
|
||
|
|
current = nil
|
||
|
|
}
|
||
|
|
current = append(current, sample.point)
|
||
|
|
}
|
||
|
|
segments = appendSolarEclipsePartialSegment(segments, current)
|
||
|
|
segments = mergeSolarEclipsePartialWrapSegment(segments, samples)
|
||
|
|
|
||
|
|
if len(segments) == 1 && len(segments[0]) > 2 && !solarEclipsePathCrossesAntimeridian(segments[0][len(segments[0])-1], segments[0][0]) {
|
||
|
|
segments[0] = append(segments[0], segments[0][0])
|
||
|
|
return segments, true
|
||
|
|
}
|
||
|
|
return segments, false
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendSolarEclipsePartialSegment(
|
||
|
|
segments [][]SolarEclipsePathPoint,
|
||
|
|
segment []SolarEclipsePathPoint,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(segment) == 0 {
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
return append(segments, segment)
|
||
|
|
}
|
||
|
|
|
||
|
|
func mergeSolarEclipsePartialWrapSegment(
|
||
|
|
segments [][]SolarEclipsePathPoint,
|
||
|
|
samples []solarEclipsePartialBoundarySample,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(segments) < 2 || len(samples) == 0 || !samples[0].ok || !samples[len(samples)-1].ok {
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
first := segments[0]
|
||
|
|
last := segments[len(segments)-1]
|
||
|
|
if solarEclipsePathCrossesAntimeridian(last[len(last)-1], first[0]) {
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
|
||
|
|
merged := make([]SolarEclipsePathPoint, 0, len(last)+len(first))
|
||
|
|
merged = append(merged, last...)
|
||
|
|
merged = append(merged, first...)
|
||
|
|
result := make([][]SolarEclipsePathPoint, 0, len(segments)-1)
|
||
|
|
result = append(result, merged)
|
||
|
|
result = append(result, segments[1:len(segments)-1]...)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipsePathCrossesAntimeridian(a, b SolarEclipsePathPoint) bool {
|
||
|
|
return math.Abs(a.Longitude-b.Longitude) > 180
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipsePathDistanceKM(a, b SolarEclipsePathPoint) float64 {
|
||
|
|
lat1 := a.Latitude * rad
|
||
|
|
lat2 := b.Latitude * rad
|
||
|
|
dlat := lat2 - lat1
|
||
|
|
dlon := solarEclipseNormalizeSignedRadians((b.Longitude - a.Longitude) * rad)
|
||
|
|
h := math.Sin(dlat/2)*math.Sin(dlat/2) +
|
||
|
|
math.Cos(lat1)*math.Cos(lat2)*math.Sin(dlon/2)*math.Sin(dlon/2)
|
||
|
|
if h > 1 {
|
||
|
|
h = 1
|
||
|
|
}
|
||
|
|
return 2 * solarEclipseEarthEquatorialRadiusKM * math.Asin(math.Sqrt(h))
|
||
|
|
}
|