257 lines
8.4 KiB
Go
257 lines
8.4 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import "math"
|
||
|
|
|
||
|
|
const (
|
||
|
|
solarEclipseCentralEnvelopeArcStepDegrees = 0.4
|
||
|
|
solarEclipseCentralEnvelopeMinArcStepDegrees = 0.01
|
||
|
|
solarEclipseCentralEnvelopeMaxArcSteps = 2000
|
||
|
|
solarEclipseCentralEnvelopeMaxSpacingKM = 48.0
|
||
|
|
solarEclipseCentralEnvelopeEndDistanceKM = 50.0
|
||
|
|
solarEclipseTotalEnvelopeTargetSpacingKM = 200.0
|
||
|
|
)
|
||
|
|
|
||
|
|
// centralTwoLimitBandEnvelope follows both continuous local-centrality limits
|
||
|
|
// between the solved sunrise and sunset closures. Unlike a union of discrete
|
||
|
|
// shadow footprints, every non-horizon point satisfies C=0 and dC/dt=0.
|
||
|
|
func (solver solarEclipseSolver) centralTwoLimitBandEnvelope(
|
||
|
|
closures [][]SolarEclipsePathPoint,
|
||
|
|
referenceJDE float64,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
branches := solver.centralBandVectorBranches(closures, referenceJDE)
|
||
|
|
if len(branches) != 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return joinSolarEclipseCentralBandBranches(branches, closures)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralBandVectorBranches(
|
||
|
|
closures [][]SolarEclipsePathPoint,
|
||
|
|
referenceJDE float64,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(closures) != 2 || len(closures[0]) < 2 || len(closures[1]) < 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
startRoots := []SolarEclipsePathPoint{closures[0][0], closures[0][len(closures[0])-1]}
|
||
|
|
endRoots := []SolarEclipsePathPoint{closures[1][0], closures[1][len(closures[1])-1]}
|
||
|
|
branches := make([][]SolarEclipsePathPoint, 2)
|
||
|
|
endIndices := [2]int{-1, -1}
|
||
|
|
solver = solver.withLocalEphemeris()
|
||
|
|
for index, root := range startRoots {
|
||
|
|
var transitions []SolarEclipsePathPoint
|
||
|
|
branch, endIndex, ok := solver.traceCentralBandVectorEnvelope(root, endRoots, &transitions, referenceJDE)
|
||
|
|
if !ok {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
branches[index], endIndices[index] = branch, endIndex
|
||
|
|
}
|
||
|
|
if endIndices[0] == endIndices[1] {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return branches
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseTotalBandEnvelope(
|
||
|
|
segments, closures [][]SolarEclipsePathPoint,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(segments) != 2 || len(segments[0]) < 2 || len(segments[1]) < 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
branches := make([][]SolarEclipsePathPoint, 2)
|
||
|
|
for index, segment := range segments {
|
||
|
|
branch := append([]SolarEclipsePathPoint(nil), segment...)
|
||
|
|
if branch[0].JDE > branch[len(branch)-1].JDE {
|
||
|
|
for left, right := 0, len(branch)-1; left < right; left, right = left+1, right-1 {
|
||
|
|
branch[left], branch[right] = branch[right], branch[left]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
branches[index] = branch
|
||
|
|
}
|
||
|
|
if len(closures) == 2 && len(closures[0]) > 1 && len(closures[1]) > 1 &&
|
||
|
|
solarEclipsePathDistanceKM(closures[0][0], closures[1][0]) < 0.01 &&
|
||
|
|
solarEclipsePathDistanceKM(closures[0][len(closures[0])-1], closures[1][len(closures[1])-1]) < 0.01 {
|
||
|
|
ring := append([]SolarEclipsePathPoint{}, branches[0]...)
|
||
|
|
for index := len(branches[1]) - 1; index >= 0; index-- {
|
||
|
|
ring = append(ring, branches[1][index])
|
||
|
|
}
|
||
|
|
ring = deduplicateSolarEclipsePathPoints(ring)
|
||
|
|
if len(ring) >= 4 {
|
||
|
|
ring = append(ring, ring[0])
|
||
|
|
return [][]SolarEclipsePathPoint{ring}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return joinSolarEclipseCentralBandBranches(branches, closures)
|
||
|
|
}
|
||
|
|
|
||
|
|
func joinSolarEclipseCentralBandBranches(
|
||
|
|
branches, closures [][]SolarEclipsePathPoint,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(branches) != 2 || len(branches[0]) == 0 || len(branches[1]) == 0 ||
|
||
|
|
len(closures) != 2 || len(closures[0]) == 0 || len(closures[1]) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
endClosure, ok := orientSolarEclipsePath(
|
||
|
|
closures[1], branches[0][len(branches[0])-1], branches[1][len(branches[1])-1],
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
startClosure, ok := orientSolarEclipsePath(
|
||
|
|
closures[0], branches[1][0], branches[0][0],
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
ring := make([]SolarEclipsePathPoint, 0,
|
||
|
|
len(branches[0])+len(branches[1])+len(startClosure)+len(endClosure),
|
||
|
|
)
|
||
|
|
ring = append(ring, branches[0]...)
|
||
|
|
ring = append(ring, endClosure[1:]...)
|
||
|
|
for index := len(branches[1]) - 2; index >= 0; index-- {
|
||
|
|
ring = append(ring, branches[1][index])
|
||
|
|
}
|
||
|
|
ring = append(ring, startClosure[1:]...)
|
||
|
|
ring = deduplicateSolarEclipsePathPoints(ring)
|
||
|
|
if len(ring) < 4 || solarEclipsePathDistanceKM(ring[0], ring[len(ring)-1]) > 0.01 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
ring[len(ring)-1] = ring[0]
|
||
|
|
return [][]SolarEclipsePathPoint{ring}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) traceCentralBandEnvelope(
|
||
|
|
root SolarEclipsePathPoint,
|
||
|
|
endRoots []SolarEclipsePathPoint,
|
||
|
|
referenceJDE float64,
|
||
|
|
) ([]SolarEclipsePathPoint, int, bool) {
|
||
|
|
state, ok := solver.nonCentralBandStateAt(root, referenceJDE)
|
||
|
|
if !ok {
|
||
|
|
return nil, -1, false
|
||
|
|
}
|
||
|
|
step := solarEclipseCentralEnvelopeArcStepDegrees
|
||
|
|
next, ok := solver.centralBandEnvelopeVisibleStart(state, step, referenceJDE)
|
||
|
|
if !ok {
|
||
|
|
return []SolarEclipsePathPoint{root}, -1, false
|
||
|
|
}
|
||
|
|
if dotSolarEclipse3(next.tangent, subtractSolarEclipse3(next.coordinates, state.coordinates)) < 0 {
|
||
|
|
for index := range next.tangent {
|
||
|
|
next.tangent[index] = -next.tangent[index]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
points := []SolarEclipsePathPoint{root, next.point}
|
||
|
|
state = next
|
||
|
|
for count := 0; count < solarEclipseCentralEnvelopeMaxArcSteps; count++ {
|
||
|
|
predictor := state.coordinates
|
||
|
|
for index := range predictor {
|
||
|
|
predictor[index] += step * state.tangent[index]
|
||
|
|
}
|
||
|
|
candidate, iterations, candidateOK := solver.correctNonCentralBandBoundary(
|
||
|
|
predictor, state.tangent, referenceJDE,
|
||
|
|
)
|
||
|
|
if !candidateOK {
|
||
|
|
step /= 2
|
||
|
|
if step < solarEclipseCentralEnvelopeMinArcStepDegrees {
|
||
|
|
return points, -1, false
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if dotSolarEclipse3(candidate.tangent, state.tangent) < 0 {
|
||
|
|
for index := range candidate.tangent {
|
||
|
|
candidate.tangent[index] = -candidate.tangent[index]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
distance := solarEclipsePathDistanceKM(state.point, candidate.point)
|
||
|
|
if distance > solarEclipseCentralEnvelopeMaxSpacingKM {
|
||
|
|
step /= 2
|
||
|
|
if step < solarEclipseCentralEnvelopeMinArcStepDegrees {
|
||
|
|
return points, -1, false
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if candidate.point.SunAltitude < 0 {
|
||
|
|
endIndex, endDistance := nearestSolarEclipsePathPoint(state.point, candidate.point, endRoots)
|
||
|
|
if endIndex < 0 || endDistance > solarEclipseCentralEnvelopeEndDistanceKM {
|
||
|
|
return points, endIndex, false
|
||
|
|
}
|
||
|
|
points = append(points, endRoots[endIndex])
|
||
|
|
return points, endIndex, true
|
||
|
|
}
|
||
|
|
points = append(points, candidate.point)
|
||
|
|
state = candidate
|
||
|
|
if distance < solarEclipseCentralEnvelopeMaxSpacingKM/2 && iterations <= 4 {
|
||
|
|
step = math.Min(solarEclipseCentralEnvelopeArcStepDegrees, step*1.5)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return points, -1, false
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralBandEnvelopeVisibleStart(
|
||
|
|
state solarEclipseNonCentralBandState,
|
||
|
|
step, referenceJDE float64,
|
||
|
|
) (solarEclipseNonCentralBandState, bool) {
|
||
|
|
best := solarEclipseNonCentralBandState{}
|
||
|
|
found := false
|
||
|
|
for trialStep := step; trialStep >= solarEclipseCentralEnvelopeMinArcStepDegrees; trialStep /= 2 {
|
||
|
|
for _, direction := range []float64{1, -1} {
|
||
|
|
predictor := state.coordinates
|
||
|
|
tangent := state.tangent
|
||
|
|
for index := range predictor {
|
||
|
|
tangent[index] *= direction
|
||
|
|
predictor[index] += trialStep * tangent[index]
|
||
|
|
}
|
||
|
|
candidate, _, ok := solver.correctNonCentralBandBoundary(predictor, tangent, referenceJDE)
|
||
|
|
if ok && candidate.point.SunAltitude > 0 &&
|
||
|
|
(!found || candidate.point.SunAltitude > best.point.SunAltitude) {
|
||
|
|
best, found = candidate, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if found {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return best, found
|
||
|
|
}
|
||
|
|
|
||
|
|
func nearestSolarEclipsePathPoint(
|
||
|
|
first, second SolarEclipsePathPoint,
|
||
|
|
points []SolarEclipsePathPoint,
|
||
|
|
) (int, float64) {
|
||
|
|
bestIndex, bestDistance := -1, math.Inf(1)
|
||
|
|
for index, point := range points {
|
||
|
|
distance := math.Min(
|
||
|
|
solarEclipsePathDistanceKM(first, point),
|
||
|
|
solarEclipsePathDistanceKM(second, point),
|
||
|
|
)
|
||
|
|
if distance < bestDistance {
|
||
|
|
bestIndex, bestDistance = index, distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return bestIndex, bestDistance
|
||
|
|
}
|
||
|
|
|
||
|
|
func orientSolarEclipsePath(
|
||
|
|
points []SolarEclipsePathPoint,
|
||
|
|
start, end SolarEclipsePathPoint,
|
||
|
|
) ([]SolarEclipsePathPoint, bool) {
|
||
|
|
if len(points) < 2 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
oriented := append([]SolarEclipsePathPoint(nil), points...)
|
||
|
|
direct := solarEclipsePathDistanceKM(start, oriented[0]) +
|
||
|
|
solarEclipsePathDistanceKM(end, oriented[len(oriented)-1])
|
||
|
|
reverse := solarEclipsePathDistanceKM(start, oriented[len(oriented)-1]) +
|
||
|
|
solarEclipsePathDistanceKM(end, oriented[0])
|
||
|
|
if reverse < direct {
|
||
|
|
for left, right := 0, len(oriented)-1; left < right; left, right = left+1, right-1 {
|
||
|
|
oriented[left], oriented[right] = oriented[right], oriented[left]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if solarEclipsePathDistanceKM(start, oriented[0]) > 0.1 ||
|
||
|
|
solarEclipsePathDistanceKM(end, oriented[len(oriented)-1]) > 0.1 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
oriented[0], oriented[len(oriented)-1] = start, end
|
||
|
|
return oriented, true
|
||
|
|
}
|