16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
1032 lines
34 KiB
Go
1032 lines
34 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
const (
|
|
solarEclipseRiseSetArcTimeScale = 360.0
|
|
solarEclipseRiseSetArcInitialStep = 4.0
|
|
solarEclipseRiseSetArcMinimumStep = 0.0025
|
|
solarEclipseRiseSetArcMaximumSteps = 8000
|
|
solarEclipseRiseSetArcTargetSpacing = 500.0
|
|
solarEclipseRiseSetArcCloseDistance = 550.0
|
|
solarEclipseRiseSetArcCloseTimeDays = 5.0 / 1440.0
|
|
solarEclipseRiseSetArcSeedDistance = 550.0
|
|
solarEclipseRiseSetArcSeedTimeDays = 30.0 / 1440.0
|
|
)
|
|
|
|
type solarEclipseRiseSetArcState struct {
|
|
coordinates [3]float64
|
|
tangent [3]float64
|
|
point SolarEclipsePathPoint
|
|
}
|
|
|
|
type solarEclipseRiseSetArcSegment struct {
|
|
key solarEclipseRiseSetCurveKey
|
|
points []SolarEclipsePathPoint
|
|
}
|
|
|
|
type solarEclipseRiseSetArcTransition uint8
|
|
|
|
const (
|
|
solarEclipseRiseSetArcNoTransition solarEclipseRiseSetArcTransition = iota
|
|
solarEclipseRiseSetArcPhaseTransition
|
|
solarEclipseRiseSetArcDirectionTransition
|
|
solarEclipseRiseSetArcFoldTransition
|
|
)
|
|
|
|
func solarEclipseRiseSetCurveTopologyComplete(curves []SolarEclipseRiseSetCurve) bool {
|
|
if len(curves) != 6 {
|
|
return false
|
|
}
|
|
keys := make(map[solarEclipseRiseSetCurveKey]bool, 6)
|
|
for curveIndex, curve := range curves {
|
|
key := solarEclipseRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}
|
|
if keys[key] || len(curve.Segments) == 0 || len(curve.Segments) > 16 {
|
|
return false
|
|
}
|
|
keys[key] = true
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
return false
|
|
}
|
|
for pointIndex := 1; pointIndex < len(segment); pointIndex++ {
|
|
if segment[pointIndex].JDE <= segment[pointIndex-1].JDE+solarEclipseRiseSetTimeEpsilonDays {
|
|
return false
|
|
}
|
|
}
|
|
for _, pointIndex := range []int{0, len(segment) - 1} {
|
|
if !solarEclipseRiseSetArcEndpointShared(curves, curveIndex, segmentIndex, pointIndex) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return len(keys) == 6
|
|
}
|
|
|
|
func (solver solarEclipseSolver) traceRiseSetCurveTopology(
|
|
seedCurves []SolarEclipseRiseSetCurve,
|
|
startJDE, endJDE, referenceJDE float64,
|
|
) []SolarEclipseRiseSetCurve {
|
|
if len(seedCurves) == 0 {
|
|
return nil
|
|
}
|
|
type component struct {
|
|
greatest bool
|
|
states []solarEclipseRiseSetArcState
|
|
}
|
|
var components []component
|
|
for _, curve := range seedCurves {
|
|
greatest := curve.Phase == RiseSetPhaseGreatest
|
|
for _, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
continue
|
|
}
|
|
seed := segment[len(segment)/2]
|
|
covered := false
|
|
for _, existing := range components {
|
|
if existing.greatest != greatest || !solarEclipseRiseSetArcContainsSeed(existing.states, seed) {
|
|
continue
|
|
}
|
|
covered = true
|
|
break
|
|
}
|
|
if covered {
|
|
continue
|
|
}
|
|
states := solver.traceRiseSetArcComponent(seed, greatest, referenceJDE, startJDE, endJDE)
|
|
if len(states) < 3 {
|
|
continue
|
|
}
|
|
components = append(components, component{greatest: greatest, states: states})
|
|
}
|
|
}
|
|
|
|
var traced []solarEclipseRiseSetArcSegment
|
|
for _, component := range components {
|
|
traced = append(traced, solver.splitRiseSetArcComponent(component.states, component.greatest)...)
|
|
}
|
|
if len(traced) == 0 {
|
|
return nil
|
|
}
|
|
curveSegments := make(map[solarEclipseRiseSetCurveKey][][]SolarEclipsePathPoint, 6)
|
|
for _, segment := range traced {
|
|
if len(segment.points) < 2 {
|
|
continue
|
|
}
|
|
points := segment.points
|
|
if points[0].JDE > points[len(points)-1].JDE {
|
|
points = append([]SolarEclipsePathPoint(nil), points...)
|
|
for left, right := 0, len(points)-1; left < right; left, right = left+1, right-1 {
|
|
points[left], points[right] = points[right], points[left]
|
|
}
|
|
}
|
|
curveSegments[segment.key] = append(curveSegments[segment.key], points)
|
|
}
|
|
keys := []solarEclipseRiseSetCurveKey{
|
|
{RiseSetPhaseStart, RiseSetDirectionRise},
|
|
{RiseSetPhaseStart, RiseSetDirectionSet},
|
|
{RiseSetPhaseGreatest, RiseSetDirectionRise},
|
|
{RiseSetPhaseGreatest, RiseSetDirectionSet},
|
|
{RiseSetPhaseEnd, RiseSetDirectionRise},
|
|
{RiseSetPhaseEnd, RiseSetDirectionSet},
|
|
}
|
|
curves := make([]SolarEclipseRiseSetCurve, 0, len(keys))
|
|
for _, key := range keys {
|
|
segments := curveSegments[key]
|
|
if len(segments) == 0 {
|
|
continue
|
|
}
|
|
curve := SolarEclipseRiseSetCurve{Phase: key.phase, Direction: key.direction, Segments: segments}
|
|
normalizeSolarEclipseRiseSetCurveSegments(&curve)
|
|
mergeSolarEclipseRiseSetArcContinuations(&curve)
|
|
if len(curve.Segments) > 0 {
|
|
curves = append(curves, curve)
|
|
}
|
|
}
|
|
return curves
|
|
}
|
|
|
|
func mergeSolarEclipseRiseSetArcContinuations(curve *SolarEclipseRiseSetCurve) {
|
|
if curve == nil || len(curve.Segments) < 2 {
|
|
return
|
|
}
|
|
for {
|
|
merged := false
|
|
for firstIndex := 0; firstIndex < len(curve.Segments) && !merged; firstIndex++ {
|
|
first := curve.Segments[firstIndex]
|
|
if len(first) < 2 {
|
|
continue
|
|
}
|
|
for secondIndex := 0; secondIndex < len(curve.Segments); secondIndex++ {
|
|
if firstIndex == secondIndex {
|
|
continue
|
|
}
|
|
second := curve.Segments[secondIndex]
|
|
if len(second) < 2 ||
|
|
math.Abs(second[0].JDE-first[len(first)-1].JDE) > solarEclipseRiseSetAttachmentTimeToleranceDays ||
|
|
solarEclipsePathDistanceKM(first[len(first)-1], second[0]) > solarEclipseRiseSetAttachmentDistanceToleranceKM {
|
|
continue
|
|
}
|
|
joined := make([]SolarEclipsePathPoint, 0, len(first)+len(second))
|
|
switch {
|
|
case second[1].JDE > first[len(first)-1].JDE+solarEclipseRiseSetTimeEpsilonDays:
|
|
joined = append(joined, first...)
|
|
joined = append(joined, second[1:]...)
|
|
case second[0].JDE > first[len(first)-2].JDE+solarEclipseRiseSetTimeEpsilonDays:
|
|
joined = append(joined, first[:len(first)-1]...)
|
|
joined = append(joined, second...)
|
|
default:
|
|
continue
|
|
}
|
|
curve.Segments[firstIndex] = joined
|
|
curve.Segments = append(curve.Segments[:secondIndex], curve.Segments[secondIndex+1:]...)
|
|
merged = true
|
|
break
|
|
}
|
|
}
|
|
if !merged {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func snapSolarEclipseRiseSetArcPhaseJunctions(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
junctions []solarEclipseRiseSetPhaseJunction,
|
|
) {
|
|
const (
|
|
maximumTimeDays = 1.0 / 1440.0
|
|
maximumDistance = 1000.0
|
|
)
|
|
for _, junction := range junctions {
|
|
for curveIndex := range curves {
|
|
curve := &curves[curveIndex]
|
|
if curve.Direction != junction.direction {
|
|
continue
|
|
}
|
|
if solarEclipseRiseSetArcPointInCurve(junction.point, *curve) {
|
|
continue
|
|
}
|
|
bestSegment, bestPoint := -1, -1
|
|
bestMetric := math.Inf(1)
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
continue
|
|
}
|
|
for _, pointIndex := range []int{0, len(segment) - 1} {
|
|
if solarEclipseRiseSetArcEndpointShared(curves, curveIndex, segmentIndex, pointIndex) {
|
|
continue
|
|
}
|
|
point := segment[pointIndex]
|
|
deltaDays := math.Abs(point.JDE - junction.point.JDE)
|
|
distance := solarEclipsePathDistanceKM(point, junction.point)
|
|
if deltaDays > maximumTimeDays || distance > maximumDistance {
|
|
continue
|
|
}
|
|
metric := distance + deltaDays*8640
|
|
if metric < bestMetric {
|
|
bestSegment, bestPoint, bestMetric = segmentIndex, pointIndex, metric
|
|
}
|
|
}
|
|
}
|
|
if bestSegment >= 0 {
|
|
curve.Segments[bestSegment][bestPoint] = junction.point
|
|
}
|
|
}
|
|
}
|
|
for curveIndex := range curves {
|
|
normalizeSolarEclipseRiseSetCurveSegments(&curves[curveIndex])
|
|
mergeSolarEclipseRiseSetArcContinuations(&curves[curveIndex])
|
|
}
|
|
}
|
|
|
|
func (solver solarEclipseSolver) closeSolarEclipseRiseSetArcFolds(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
junctions []solarEclipseRiseSetPhaseJunction,
|
|
) {
|
|
const (
|
|
maximumFoldTimeDays = 2.0 / 1440.0
|
|
maximumFoldDistance = 1500.0
|
|
maximumBridgeDays = 5.0 / 1440.0
|
|
maximumBridgeKM = 3000.0
|
|
)
|
|
for curveIndex := range curves {
|
|
curve := &curves[curveIndex]
|
|
greatest := curve.Phase == RiseSetPhaseGreatest
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
continue
|
|
}
|
|
for _, pointIndex := range []int{0, len(segment) - 1} {
|
|
endpoint := curve.Segments[segmentIndex][pointIndex]
|
|
if solarEclipseRiseSetArcEndpointShared(curves, curveIndex, segmentIndex, pointIndex) {
|
|
continue
|
|
}
|
|
fold, ok := solver.refineRiseSetFoldPoint(
|
|
endpoint.JDE, endpoint.Longitude, endpoint.Latitude, greatest,
|
|
)
|
|
if !ok {
|
|
for _, junction := range junctions {
|
|
if junction.direction != curve.Direction ||
|
|
math.Abs(junction.point.JDE-endpoint.JDE) > maximumBridgeDays ||
|
|
solarEclipsePathDistanceKM(junction.point, endpoint) > maximumBridgeKM {
|
|
continue
|
|
}
|
|
fold, ok = solver.refineRiseSetFold(junction.point, endpoint, greatest)
|
|
if ok {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !ok || math.Abs(fold.JDE-endpoint.JDE) > maximumFoldTimeDays ||
|
|
solarEclipsePathDistanceKM(fold, endpoint) > maximumFoldDistance {
|
|
continue
|
|
}
|
|
curve.Segments[segmentIndex][pointIndex] = fold
|
|
bestJunction := SolarEclipsePathPoint{}
|
|
bestMetric := math.Inf(1)
|
|
for _, junction := range junctions {
|
|
if junction.direction != curve.Direction ||
|
|
solarEclipseRiseSetArcPointInCurve(junction.point, *curve) {
|
|
continue
|
|
}
|
|
deltaDays := math.Abs(junction.point.JDE - fold.JDE)
|
|
distance := solarEclipsePathDistanceKM(junction.point, fold)
|
|
if deltaDays > maximumBridgeDays || distance > maximumBridgeKM {
|
|
continue
|
|
}
|
|
metric := distance + deltaDays*8640
|
|
if metric < bestMetric {
|
|
bestJunction, bestMetric = junction.point, metric
|
|
}
|
|
}
|
|
if bestMetric == math.Inf(1) {
|
|
continue
|
|
}
|
|
start, end := bestJunction, fold
|
|
if start.JDE > end.JDE {
|
|
start, end = end, start
|
|
}
|
|
bridge := solver.appendRefinedRiseSetSegment(
|
|
[]SolarEclipsePathPoint{start}, start, end,
|
|
curve.Phase, curve.Direction, 0,
|
|
)
|
|
if len(bridge) >= 2 {
|
|
curve.Segments = append(curve.Segments, bridge)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for curveIndex := range curves {
|
|
normalizeSolarEclipseRiseSetCurveSegments(&curves[curveIndex])
|
|
mergeSolarEclipseRiseSetArcContinuations(&curves[curveIndex])
|
|
}
|
|
}
|
|
|
|
func solarEclipseRiseSetArcEndpointShared(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
curveIndex, segmentIndex, pointIndex int,
|
|
) bool {
|
|
point := curves[curveIndex].Segments[segmentIndex][pointIndex]
|
|
for otherCurveIndex, curve := range curves {
|
|
for otherSegmentIndex, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
continue
|
|
}
|
|
for _, otherPointIndex := range []int{0, len(segment) - 1} {
|
|
if curveIndex == otherCurveIndex && segmentIndex == otherSegmentIndex && pointIndex == otherPointIndex {
|
|
continue
|
|
}
|
|
other := segment[otherPointIndex]
|
|
if math.Abs(point.JDE-other.JDE) <= solarEclipseRiseSetTimeEpsilonDays &&
|
|
solarEclipsePathDistanceKM(point, other) <= 0.01 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func solarEclipseRiseSetArcPointInCurve(
|
|
point SolarEclipsePathPoint,
|
|
curve SolarEclipseRiseSetCurve,
|
|
) bool {
|
|
for _, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
continue
|
|
}
|
|
for _, pointIndex := range []int{0, len(segment) - 1} {
|
|
candidate := segment[pointIndex]
|
|
if math.Abs(point.JDE-candidate.JDE) <= solarEclipseRiseSetTimeEpsilonDays &&
|
|
solarEclipsePathDistanceKM(point, candidate) <= 0.01 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (solver solarEclipseSolver) snapNearCoincidentSolarEclipseRiseSetEndpoints(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
) {
|
|
const (
|
|
maximumTimeDays = 2.0 / 86400.0
|
|
maximumDistance = 10.0
|
|
)
|
|
for curveIndex := range curves {
|
|
curve := &curves[curveIndex]
|
|
for firstSegmentIndex, firstSegment := range curve.Segments {
|
|
if len(firstSegment) < 2 {
|
|
continue
|
|
}
|
|
for secondSegmentIndex := firstSegmentIndex + 1; secondSegmentIndex < len(curve.Segments); secondSegmentIndex++ {
|
|
secondSegment := curve.Segments[secondSegmentIndex]
|
|
if len(secondSegment) < 2 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
firstPointIndex, secondPointIndex := len(firstSegment)-1, len(secondSegment)-1
|
|
if atStart {
|
|
firstPointIndex, secondPointIndex = 0, 0
|
|
}
|
|
first := curve.Segments[firstSegmentIndex][firstPointIndex]
|
|
second := curve.Segments[secondSegmentIndex][secondPointIndex]
|
|
if math.Abs(first.JDE-second.JDE) > maximumTimeDays ||
|
|
solarEclipsePathDistanceKM(first, second) > maximumDistance {
|
|
continue
|
|
}
|
|
sharedFirst := solarEclipseRiseSetArcEndpointShared(
|
|
curves, curveIndex, firstSegmentIndex, firstPointIndex,
|
|
)
|
|
sharedSecond := solarEclipseRiseSetArcEndpointShared(
|
|
curves, curveIndex, secondSegmentIndex, secondPointIndex,
|
|
)
|
|
common := first
|
|
switch {
|
|
case sharedSecond && !sharedFirst:
|
|
common = second
|
|
case !sharedFirst && !sharedSecond:
|
|
if fold, ok := solver.refineRiseSetFold(
|
|
first, second, curve.Phase == RiseSetPhaseGreatest,
|
|
); ok {
|
|
common = fold
|
|
} else {
|
|
common = solarEclipseRiseSetMidpoint(first, second)
|
|
}
|
|
}
|
|
curve.Segments[firstSegmentIndex][firstPointIndex] = common
|
|
curve.Segments[secondSegmentIndex][secondPointIndex] = common
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (solver solarEclipseSolver) closeSolarEclipseRiseSetArcDirectionJunctions(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
phaseJunctions []solarEclipseRiseSetPhaseJunction,
|
|
) {
|
|
const (
|
|
maximumTimeDays = 2.0 / 1440.0
|
|
maximumEndpointDistance = 1500.0
|
|
maximumPairDistance = 3000.0
|
|
)
|
|
curveIndices := make(map[solarEclipseRiseSetCurveKey]int, len(curves))
|
|
for index, curve := range curves {
|
|
curveIndices[solarEclipseRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
|
|
}
|
|
for curveIndex := range curves {
|
|
curve := &curves[curveIndex]
|
|
greatest := curve.Phase == RiseSetPhaseGreatest
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
continue
|
|
}
|
|
for _, pointIndex := range []int{0, len(segment) - 1} {
|
|
if solarEclipseRiseSetArcEndpointShared(curves, curveIndex, segmentIndex, pointIndex) {
|
|
continue
|
|
}
|
|
endpoint := curve.Segments[segmentIndex][pointIndex]
|
|
junction, ok := solver.refineRiseSetDirectionJunction(
|
|
endpoint.JDE, endpoint.Longitude, endpoint.Latitude, greatest,
|
|
)
|
|
if !ok || math.Abs(junction.JDE-endpoint.JDE) > maximumTimeDays ||
|
|
solarEclipsePathDistanceKM(junction, endpoint) > maximumEndpointDistance {
|
|
continue
|
|
}
|
|
oppositeDirection := RiseSetDirectionSet
|
|
if curve.Direction == RiseSetDirectionSet {
|
|
oppositeDirection = RiseSetDirectionRise
|
|
}
|
|
oppositeIndex, haveOpposite := curveIndices[solarEclipseRiseSetCurveKey{
|
|
phase: curve.Phase, direction: oppositeDirection,
|
|
}]
|
|
if !haveOpposite {
|
|
continue
|
|
}
|
|
bestSegment, bestPoint := -1, -1
|
|
bestMetric := math.Inf(1)
|
|
for otherSegmentIndex, otherSegment := range curves[oppositeIndex].Segments {
|
|
if len(otherSegment) < 2 {
|
|
continue
|
|
}
|
|
for _, otherPointIndex := range []int{0, len(otherSegment) - 1} {
|
|
other := otherSegment[otherPointIndex]
|
|
deltaDays := math.Abs(junction.JDE - other.JDE)
|
|
distance := solarEclipsePathDistanceKM(junction, other)
|
|
if deltaDays > maximumTimeDays || distance > maximumPairDistance {
|
|
continue
|
|
}
|
|
metric := distance + deltaDays*8640
|
|
if metric < bestMetric {
|
|
bestSegment, bestPoint, bestMetric = otherSegmentIndex, otherPointIndex, metric
|
|
}
|
|
}
|
|
}
|
|
if bestSegment < 0 {
|
|
bestJunction := SolarEclipsePathPoint{}
|
|
bestJunctionMetric := math.Inf(1)
|
|
for _, phaseJunction := range phaseJunctions {
|
|
if phaseJunction.direction != oppositeDirection ||
|
|
solarEclipseRiseSetArcPointInCurve(phaseJunction.point, curves[oppositeIndex]) {
|
|
continue
|
|
}
|
|
deltaDays := math.Abs(junction.JDE - phaseJunction.point.JDE)
|
|
distance := solarEclipsePathDistanceKM(junction, phaseJunction.point)
|
|
if deltaDays > maximumTimeDays || distance > maximumPairDistance {
|
|
continue
|
|
}
|
|
metric := distance + deltaDays*8640
|
|
if metric < bestJunctionMetric {
|
|
bestJunction, bestJunctionMetric = phaseJunction.point, metric
|
|
}
|
|
}
|
|
if bestJunctionMetric == math.Inf(1) {
|
|
continue
|
|
}
|
|
start, end := junction, bestJunction
|
|
if start.JDE > end.JDE {
|
|
start, end = end, start
|
|
}
|
|
bridge := solver.appendRefinedRiseSetSegment(
|
|
[]SolarEclipsePathPoint{start}, start, end,
|
|
curve.Phase, oppositeDirection, 0,
|
|
)
|
|
if len(bridge) < 2 {
|
|
continue
|
|
}
|
|
curve.Segments[segmentIndex][pointIndex] = junction
|
|
curves[oppositeIndex].Segments = append(curves[oppositeIndex].Segments, bridge)
|
|
continue
|
|
}
|
|
curve.Segments[segmentIndex][pointIndex] = junction
|
|
curves[oppositeIndex].Segments[bestSegment][bestPoint] = junction
|
|
}
|
|
}
|
|
}
|
|
for curveIndex := range curves {
|
|
normalizeSolarEclipseRiseSetCurveSegments(&curves[curveIndex])
|
|
mergeSolarEclipseRiseSetArcContinuations(&curves[curveIndex])
|
|
}
|
|
}
|
|
|
|
func solarEclipseRiseSetArcContainsSeed(states []solarEclipseRiseSetArcState, seed SolarEclipsePathPoint) bool {
|
|
for _, state := range states {
|
|
if math.Abs(state.point.JDE-seed.JDE) <= solarEclipseRiseSetArcSeedTimeDays &&
|
|
solarEclipsePathDistanceKM(state.point, seed) <= solarEclipseRiseSetArcSeedDistance {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (solver solarEclipseSolver) traceRiseSetArcComponent(
|
|
seed SolarEclipsePathPoint,
|
|
greatest bool,
|
|
referenceJDE, startJDE, endJDE float64,
|
|
) []solarEclipseRiseSetArcState {
|
|
forward, closed := solver.traceRiseSetArc(seed, greatest, 1, referenceJDE, startJDE, endJDE)
|
|
if closed {
|
|
return forward
|
|
}
|
|
backward, _ := solver.traceRiseSetArc(seed, greatest, -1, referenceJDE, startJDE, endJDE)
|
|
states := make([]solarEclipseRiseSetArcState, 0, len(backward)+len(forward)-1)
|
|
for index := len(backward) - 1; index >= 0; index-- {
|
|
state := backward[index]
|
|
for tangentIndex := range state.tangent {
|
|
state.tangent[tangentIndex] = -state.tangent[tangentIndex]
|
|
}
|
|
states = append(states, state)
|
|
}
|
|
return append(states, forward[1:]...)
|
|
}
|
|
|
|
func (solver solarEclipseSolver) traceRiseSetArc(
|
|
seed SolarEclipsePathPoint,
|
|
greatest bool,
|
|
direction int,
|
|
referenceJDE, startJDE, endJDE float64,
|
|
) ([]solarEclipseRiseSetArcState, bool) {
|
|
state, ok := solver.riseSetArcStateAt(seed, greatest, referenceJDE)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
for index := range state.tangent {
|
|
state.tangent[index] *= float64(direction)
|
|
}
|
|
initial := state
|
|
states := []solarEclipseRiseSetArcState{state}
|
|
step := solarEclipseRiseSetArcInitialStep
|
|
previousSeedPlane := 0.0
|
|
closureArmed := false
|
|
for count := 0; count < solarEclipseRiseSetArcMaximumSteps; count++ {
|
|
predictor := state.coordinates
|
|
for index := range predictor {
|
|
predictor[index] += step * state.tangent[index]
|
|
}
|
|
next, iterations, nextOK := solver.correctRiseSetArc(predictor, state.tangent, greatest, referenceJDE)
|
|
if !nextOK {
|
|
step /= 2
|
|
if step < solarEclipseRiseSetArcMinimumStep {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
if dotSolarEclipse3(next.tangent, state.tangent) < 0 {
|
|
for index := range next.tangent {
|
|
next.tangent[index] = -next.tangent[index]
|
|
}
|
|
}
|
|
distance := solarEclipsePathDistanceKM(state.point, next.point)
|
|
multipleTransitions := solver.riseSetArcTransitionCount(state, next, greatest) > 1
|
|
if distance > solarEclipseRiseSetArcTargetSpacing ||
|
|
multipleTransitions && step > 2*solarEclipseRiseSetArcMinimumStep {
|
|
step /= 2
|
|
if step < solarEclipseRiseSetArcMinimumStep && !multipleTransitions {
|
|
break
|
|
}
|
|
if step < solarEclipseRiseSetArcMinimumStep {
|
|
step = solarEclipseRiseSetArcMinimumStep
|
|
}
|
|
continue
|
|
}
|
|
if next.point.JDE < startJDE-0.01 || next.point.JDE > endJDE+0.01 {
|
|
break
|
|
}
|
|
states = append(states, next)
|
|
state = next
|
|
seedPlane := solarEclipseRiseSetArcSeedPlane(next, initial)
|
|
if seedPlane < -solarEclipseRiseSetArcMinimumStep {
|
|
closureArmed = true
|
|
}
|
|
if count > 30 && closureArmed && previousSeedPlane < 0 && seedPlane >= 0 &&
|
|
math.Abs(next.point.JDE-initial.point.JDE) <= solarEclipseRiseSetArcCloseTimeDays &&
|
|
solarEclipsePathDistanceKM(next.point, initial.point) <= solarEclipseRiseSetArcCloseDistance &&
|
|
dotSolarEclipse3(next.tangent, initial.tangent) > 0.5 {
|
|
states[len(states)-1] = initial
|
|
return states, true
|
|
}
|
|
previousSeedPlane = seedPlane
|
|
if distance < solarEclipseRiseSetArcTargetSpacing/2 && iterations <= 4 {
|
|
step = math.Min(solarEclipseRiseSetArcInitialStep, step*1.5)
|
|
}
|
|
}
|
|
return states, false
|
|
}
|
|
|
|
func solarEclipseRiseSetArcSeedPlane(
|
|
state, seed solarEclipseRiseSetArcState,
|
|
) float64 {
|
|
delta := [3]float64{
|
|
math.Remainder(state.coordinates[0]-seed.coordinates[0], 360),
|
|
state.coordinates[1] - seed.coordinates[1],
|
|
state.coordinates[2] - seed.coordinates[2],
|
|
}
|
|
return dotSolarEclipse3(delta, seed.tangent)
|
|
}
|
|
|
|
func (solver solarEclipseSolver) riseSetArcTransitionCount(
|
|
first, second solarEclipseRiseSetArcState,
|
|
greatest bool,
|
|
) int {
|
|
count := 0
|
|
if first.tangent[2]*second.tangent[2] < 0 {
|
|
count++
|
|
}
|
|
firstEvaluation := solver.magnitudeEvaluationAt(first.point.JDE)
|
|
secondEvaluation := solver.magnitudeEvaluationAt(second.point.JDE)
|
|
if firstEvaluation.sunAltitudeDerivative(first.point.Longitude, first.point.Latitude)*
|
|
secondEvaluation.sunAltitudeDerivative(second.point.Longitude, second.point.Latitude) < 0 {
|
|
count++
|
|
}
|
|
if greatest {
|
|
firstGap := solarEclipsePartialContactGap(firstEvaluation.center.stateAt(first.point.Longitude*rad, first.point.Latitude*rad, 0))
|
|
secondGap := solarEclipsePartialContactGap(secondEvaluation.center.stateAt(second.point.Longitude*rad, second.point.Latitude*rad, 0))
|
|
if firstGap*secondGap < 0 {
|
|
count++
|
|
}
|
|
} else if firstEvaluation.partialContactDerivative(first.point.Longitude, first.point.Latitude)*
|
|
secondEvaluation.partialContactDerivative(second.point.Longitude, second.point.Latitude) < 0 {
|
|
count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (solver solarEclipseSolver) riseSetArcStateAt(
|
|
point SolarEclipsePathPoint,
|
|
greatest bool,
|
|
referenceJDE float64,
|
|
) (solarEclipseRiseSetArcState, bool) {
|
|
coordinates := [3]float64{
|
|
point.Longitude,
|
|
point.Latitude,
|
|
(point.JDE - referenceJDE) * solarEclipseRiseSetArcTimeScale,
|
|
}
|
|
_, jacobian, ok := solver.riseSetArcJacobian(coordinates, greatest, referenceJDE)
|
|
if !ok {
|
|
return solarEclipseRiseSetArcState{}, false
|
|
}
|
|
tangent, ok := solarEclipseMagnitudeArcTangent(jacobian)
|
|
return solarEclipseRiseSetArcState{coordinates: coordinates, tangent: tangent, point: point}, ok
|
|
}
|
|
|
|
func (solver solarEclipseSolver) correctRiseSetArc(
|
|
predictor, tangent [3]float64,
|
|
greatest bool,
|
|
referenceJDE float64,
|
|
) (solarEclipseRiseSetArcState, int, bool) {
|
|
coordinates := predictor
|
|
for iteration := 0; iteration < 16; iteration++ {
|
|
residual, jacobian, ok := solver.riseSetArcJacobian(coordinates, greatest, referenceJDE)
|
|
if !ok {
|
|
return solarEclipseRiseSetArcState{}, iteration, false
|
|
}
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
|
if math.Abs(residual[0]) <= 1e-10 && math.Abs(residual[1]) <= 1e-10 && math.Abs(planeResidual) <= 1e-9 {
|
|
return solver.validRiseSetArcState(coordinates, jacobian, referenceJDE, iteration+1)
|
|
}
|
|
matrix := [3][3]float64{jacobian[0], jacobian[1], tangent}
|
|
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -planeResidual})
|
|
if !ok {
|
|
return solarEclipseRiseSetArcState{}, iteration, false
|
|
}
|
|
norm := math.Sqrt(dotSolarEclipse3(delta, delta))
|
|
if norm > 2 {
|
|
for index := range delta {
|
|
delta[index] *= 2 / norm
|
|
}
|
|
}
|
|
for index := range coordinates {
|
|
coordinates[index] += delta[index]
|
|
}
|
|
coordinates[0] = normalizeLongitude(coordinates[0])
|
|
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
|
|
return solarEclipseRiseSetArcState{}, iteration, false
|
|
}
|
|
}
|
|
residual, jacobian, ok := solver.riseSetArcJacobian(coordinates, greatest, referenceJDE)
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-7 || math.Abs(planeResidual) > 1e-7 {
|
|
return solarEclipseRiseSetArcState{}, 16, false
|
|
}
|
|
return solver.validRiseSetArcState(coordinates, jacobian, referenceJDE, 16)
|
|
}
|
|
|
|
func (solver solarEclipseSolver) validRiseSetArcState(
|
|
coordinates [3]float64,
|
|
jacobian [2][3]float64,
|
|
referenceJDE float64,
|
|
iterations int,
|
|
) (solarEclipseRiseSetArcState, int, bool) {
|
|
jde := referenceJDE + coordinates[2]/solarEclipseRiseSetArcTimeScale
|
|
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
|
|
evaluation := solver.magnitudeEvaluationAt(jde)
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
tangent, ok := solarEclipseMagnitudeArcTangent(jacobian)
|
|
if !ok {
|
|
return solarEclipseRiseSetArcState{}, iterations, false
|
|
}
|
|
return solarEclipseRiseSetArcState{
|
|
coordinates: coordinates,
|
|
tangent: tangent,
|
|
point: SolarEclipsePathPoint{
|
|
JDE: jde, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
|
|
},
|
|
}, iterations, true
|
|
}
|
|
|
|
func (solver solarEclipseSolver) riseSetArcJacobian(
|
|
coordinates [3]float64,
|
|
greatest bool,
|
|
referenceJDE float64,
|
|
) ([2]float64, [2][3]float64, bool) {
|
|
jde := referenceJDE + coordinates[2]/solarEclipseRiseSetArcTimeScale
|
|
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
|
|
evaluation := solver.magnitudeEvaluationAt(jde)
|
|
residual, ok := solarEclipseRiseSetArcResidualAt(evaluation, longitude, latitude, greatest)
|
|
if !ok {
|
|
return [2]float64{}, [2][3]float64{}, false
|
|
}
|
|
steps := [3]float64{1e-4, 1e-4, 5.0 * solarEclipseRiseSetArcTimeScale / 86400.0}
|
|
jacobian := [2][3]float64{}
|
|
for column, shifted := range [][2]float64{{longitude + steps[0], latitude}, {longitude, latitude + steps[1]}} {
|
|
shiftedResidual, shiftedOK := solarEclipseRiseSetArcResidualAt(evaluation, shifted[0], shifted[1], greatest)
|
|
if !shiftedOK {
|
|
return [2]float64{}, [2][3]float64{}, false
|
|
}
|
|
for row := 0; row < 2; row++ {
|
|
jacobian[row][column] = (shiftedResidual[row] - residual[row]) / steps[column]
|
|
}
|
|
}
|
|
timeEvaluation := solver.magnitudeEvaluationAt(jde + steps[2]/solarEclipseRiseSetArcTimeScale)
|
|
timeResidual, timeOK := solarEclipseRiseSetArcResidualAt(timeEvaluation, longitude, latitude, greatest)
|
|
if !timeOK {
|
|
return [2]float64{}, [2][3]float64{}, false
|
|
}
|
|
for row := 0; row < 2; row++ {
|
|
jacobian[row][2] = (timeResidual[row] - residual[row]) / steps[2]
|
|
}
|
|
return residual, jacobian, true
|
|
}
|
|
|
|
func solarEclipseRiseSetArcResidualAt(
|
|
evaluation solarEclipseRiseSetEvaluation,
|
|
longitude, latitude float64,
|
|
greatest bool,
|
|
) ([2]float64, bool) {
|
|
// 非 greatest 相位残差就是同一个 stateAt 的中心距盈余,复用该状态而不是再算一次。
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
phase := 0.0
|
|
if greatest {
|
|
phase = evaluation.separationDerivative(longitude, latitude)
|
|
} else {
|
|
phase = solarEclipsePartialContactGap(state)
|
|
}
|
|
return [2]float64{phase, state.sunAltitudeRad},
|
|
finite(phase) && finite(state.sunAltitudeRad)
|
|
}
|
|
|
|
func (solver solarEclipseSolver) splitRiseSetArcComponent(
|
|
states []solarEclipseRiseSetArcState,
|
|
greatest bool,
|
|
) []solarEclipseRiseSetArcSegment {
|
|
if len(states) < 2 {
|
|
return nil
|
|
}
|
|
key, valid := solver.riseSetArcKey(states[0], greatest)
|
|
var segments []solarEclipseRiseSetArcSegment
|
|
current := solarEclipseRiseSetArcSegment{key: key}
|
|
if valid {
|
|
current.points = append(current.points, states[0].point)
|
|
}
|
|
for index := 0; index < len(states)-1; index++ {
|
|
first, second := states[index], states[index+1]
|
|
transition, point := solver.riseSetArcTransition(first, second, greatest)
|
|
if transition == solarEclipseRiseSetArcNoTransition {
|
|
if valid {
|
|
current.points = append(current.points, second.point)
|
|
}
|
|
continue
|
|
}
|
|
if valid {
|
|
current.points = append(current.points, point)
|
|
if len(current.points) >= 2 {
|
|
segments = append(segments, current)
|
|
}
|
|
}
|
|
switch transition {
|
|
case solarEclipseRiseSetArcPhaseTransition:
|
|
if greatest {
|
|
valid = !valid
|
|
} else if key.phase == RiseSetPhaseStart {
|
|
key.phase = RiseSetPhaseEnd
|
|
} else {
|
|
key.phase = RiseSetPhaseStart
|
|
}
|
|
case solarEclipseRiseSetArcDirectionTransition:
|
|
if key.direction == RiseSetDirectionRise {
|
|
key.direction = RiseSetDirectionSet
|
|
} else {
|
|
key.direction = RiseSetDirectionRise
|
|
}
|
|
case solarEclipseRiseSetArcFoldTransition:
|
|
}
|
|
if classifiedKey, classifiedValid := solver.riseSetArcKey(second, greatest); classifiedValid {
|
|
key, valid = classifiedKey, true
|
|
} else if greatest {
|
|
valid = false
|
|
}
|
|
current = solarEclipseRiseSetArcSegment{key: key}
|
|
if valid {
|
|
current.points = append(current.points, point, second.point)
|
|
}
|
|
}
|
|
if valid && len(current.points) >= 2 {
|
|
segments = append(segments, current)
|
|
}
|
|
if len(segments) >= 2 && states[0].point == states[len(states)-1].point &&
|
|
segments[0].key == segments[len(segments)-1].key {
|
|
last := segments[len(segments)-1]
|
|
first := segments[0]
|
|
last.points = append(last.points[:len(last.points)-1], first.points...)
|
|
segments[0] = last
|
|
segments = segments[:len(segments)-1]
|
|
}
|
|
return segments
|
|
}
|
|
|
|
func (solver solarEclipseSolver) riseSetArcKey(
|
|
state solarEclipseRiseSetArcState,
|
|
greatest bool,
|
|
) (solarEclipseRiseSetCurveKey, bool) {
|
|
evaluation := solver.magnitudeEvaluationAt(state.point.JDE)
|
|
if greatest {
|
|
longitude, latitude := state.point.Longitude, state.point.Latitude
|
|
local := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
altitudeDerivative := evaluation.sunAltitudeDerivative(longitude, latitude)
|
|
if solarEclipsePartialContactGap(local) > 0 ||
|
|
evaluation.separationSecondDerivative(longitude, latitude) <= 0 ||
|
|
!finite(altitudeDerivative) || math.Abs(altitudeDerivative) < 1e-10 {
|
|
return solarEclipseRiseSetCurveKey{}, false
|
|
}
|
|
direction := RiseSetDirectionSet
|
|
if altitudeDerivative > 0 {
|
|
direction = RiseSetDirectionRise
|
|
}
|
|
return solarEclipseRiseSetCurveKey{phase: RiseSetPhaseGreatest, direction: direction}, true
|
|
}
|
|
point, key, valid := evaluation.classify(state.point.Longitude, state.point.Latitude, greatest)
|
|
_ = point
|
|
return key, valid
|
|
}
|
|
|
|
func (solver solarEclipseSolver) riseSetArcTransition(
|
|
first, second solarEclipseRiseSetArcState,
|
|
greatest bool,
|
|
) (solarEclipseRiseSetArcTransition, SolarEclipsePathPoint) {
|
|
firstEvaluation := solver.magnitudeEvaluationAt(first.point.JDE)
|
|
secondEvaluation := solver.magnitudeEvaluationAt(second.point.JDE)
|
|
if greatest {
|
|
firstGap := solarEclipsePartialContactGap(firstEvaluation.center.stateAt(first.point.Longitude*rad, first.point.Latitude*rad, 0))
|
|
secondGap := solarEclipsePartialContactGap(secondEvaluation.center.stateAt(second.point.Longitude*rad, second.point.Latitude*rad, 0))
|
|
if firstGap*secondGap <= 0 {
|
|
if point, ok := solver.refineRiseSetPhaseJunctionOnHorizon(solarEclipseRiseSetMidpoint(first.point, second.point)); ok {
|
|
return solarEclipseRiseSetArcPhaseTransition, point
|
|
}
|
|
}
|
|
} else {
|
|
firstDerivative := firstEvaluation.partialContactDerivative(first.point.Longitude, first.point.Latitude)
|
|
secondDerivative := secondEvaluation.partialContactDerivative(second.point.Longitude, second.point.Latitude)
|
|
if firstDerivative*secondDerivative <= 0 {
|
|
if point, ok := solver.refineRiseSetPhaseJunctionOnHorizon(solarEclipseRiseSetMidpoint(first.point, second.point)); ok {
|
|
return solarEclipseRiseSetArcPhaseTransition, point
|
|
}
|
|
}
|
|
}
|
|
firstAltitudeDerivative := firstEvaluation.sunAltitudeDerivative(first.point.Longitude, first.point.Latitude)
|
|
secondAltitudeDerivative := secondEvaluation.sunAltitudeDerivative(second.point.Longitude, second.point.Latitude)
|
|
if firstAltitudeDerivative*secondAltitudeDerivative <= 0 {
|
|
if point, ok := solver.refineRiseSetDirectionJunction(
|
|
(first.point.JDE+second.point.JDE)/2,
|
|
normalizeLongitude(first.point.Longitude+math.Remainder(second.point.Longitude-first.point.Longitude, 360)/2),
|
|
(first.point.Latitude+second.point.Latitude)/2,
|
|
greatest,
|
|
); ok {
|
|
return solarEclipseRiseSetArcDirectionTransition, point
|
|
}
|
|
}
|
|
if first.tangent[2]*second.tangent[2] <= 0 {
|
|
if point, ok := solver.refineRiseSetFold(first.point, second.point, greatest); ok {
|
|
return solarEclipseRiseSetArcFoldTransition, point
|
|
}
|
|
}
|
|
return solarEclipseRiseSetArcNoTransition, SolarEclipsePathPoint{}
|
|
}
|
|
|
|
func sortSolarEclipseRiseSetSegments(curves []SolarEclipseRiseSetCurve) {
|
|
for curveIndex := range curves {
|
|
sort.Slice(curves[curveIndex].Segments, func(first, second int) bool {
|
|
return curves[curveIndex].Segments[first][0].JDE < curves[curveIndex].Segments[second][0].JDE
|
|
})
|
|
}
|
|
}
|
|
|
|
func deduplicateSolarEclipseRiseSetArcSegments(curves []SolarEclipseRiseSetCurve) {
|
|
for curveIndex := range curves {
|
|
segments := curves[curveIndex].Segments
|
|
unique := make([][]SolarEclipsePathPoint, 0, len(segments))
|
|
for _, segment := range segments {
|
|
duplicate := false
|
|
for _, existing := range unique {
|
|
if solarEclipseRiseSetArcSegmentsEquivalent(segment, existing) {
|
|
duplicate = true
|
|
break
|
|
}
|
|
}
|
|
if !duplicate {
|
|
unique = append(unique, segment)
|
|
}
|
|
}
|
|
curves[curveIndex].Segments = unique
|
|
}
|
|
}
|
|
|
|
func solarEclipseRiseSetArcSegmentsEquivalent(
|
|
first, second []SolarEclipsePathPoint,
|
|
) bool {
|
|
if len(first) < 2 || len(second) < 2 {
|
|
return false
|
|
}
|
|
for _, endpoints := range [][2]SolarEclipsePathPoint{
|
|
{first[0], second[0]},
|
|
{first[len(first)-1], second[len(second)-1]},
|
|
} {
|
|
if math.Abs(endpoints[0].JDE-endpoints[1].JDE) > 1.0/86400.0 ||
|
|
solarEclipsePathDistanceKM(endpoints[0], endpoints[1]) > 1 {
|
|
return false
|
|
}
|
|
}
|
|
for _, fraction := range []float64{0.25, 0.5, 0.75} {
|
|
jd := first[0].JDE + fraction*(first[len(first)-1].JDE-first[0].JDE)
|
|
firstPoint, firstOK := solarEclipseRiseSetArcPointAtTime(first, jd)
|
|
secondPoint, secondOK := solarEclipseRiseSetArcPointAtTime(second, jd)
|
|
if !firstOK || !secondOK || solarEclipsePathDistanceKM(firstPoint, secondPoint) > 250 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func solarEclipseRiseSetArcPointAtTime(
|
|
segment []SolarEclipsePathPoint,
|
|
jd float64,
|
|
) (SolarEclipsePathPoint, bool) {
|
|
if len(segment) < 2 || jd < segment[0].JDE || jd > segment[len(segment)-1].JDE {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
index := sort.Search(len(segment), func(index int) bool { return segment[index].JDE >= jd })
|
|
if index == 0 {
|
|
return segment[0], true
|
|
}
|
|
if index >= len(segment) {
|
|
return segment[len(segment)-1], true
|
|
}
|
|
before, after := segment[index-1], segment[index]
|
|
if after.JDE <= before.JDE {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
fraction := (jd - before.JDE) / (after.JDE - before.JDE)
|
|
return SolarEclipsePathPoint{
|
|
JDE: jd,
|
|
Longitude: normalizeLongitude(
|
|
before.Longitude + fraction*math.Remainder(after.Longitude-before.Longitude, 360),
|
|
),
|
|
Latitude: before.Latitude + fraction*(after.Latitude-before.Latitude),
|
|
}, true
|
|
}
|