2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
654 lines
22 KiB
Go
654 lines
22 KiB
Go
package basic
|
|
|
|
import "math"
|
|
|
|
func (solver solarEclipseSolver) completeRiseSetCurveEndpoints(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
stepDays float64,
|
|
phaseJunctions []solarEclipseRiseSetPhaseJunction,
|
|
) {
|
|
for index := range curves {
|
|
solver.completeRiseSetFoldEndpoints(&curves[index], stepDays)
|
|
}
|
|
solver.completeRiseSetPhaseJunctions(curves, stepDays, phaseJunctions)
|
|
solver.completeRiseSetDirectionJunctions(curves, stepDays)
|
|
for index := range curves {
|
|
solver.refineRiseSetCurveSpacing(&curves[index])
|
|
normalizeSolarEclipseRiseSetCurveSegments(&curves[index])
|
|
}
|
|
}
|
|
|
|
// normalizeSolarEclipseRiseSetCurveSegments keeps each rendered branch
|
|
// strictly time-ordered. Endpoint completion can discover a real horizon fold
|
|
// after the sampled branch was built; that fold belongs to a separate branch,
|
|
// not to a reversed segment. Near-identical roots are numerical duplicates.
|
|
func normalizeSolarEclipseRiseSetCurveSegments(curve *SolarEclipseRiseSetCurve) {
|
|
if curve == nil {
|
|
return
|
|
}
|
|
segments := make([][]SolarEclipsePathPoint, 0, len(curve.Segments))
|
|
for _, segment := range curve.Segments {
|
|
segments = append(segments, splitSolarEclipseRiseSetTimeFolds(segment)...)
|
|
}
|
|
curve.Segments = segments
|
|
}
|
|
|
|
// splitSolarEclipseRiseSetTimeFolds preserves spatial branches when a
|
|
// horizon curve folds in time. A rendered segment must be strictly increasing
|
|
// in JDE, but the physical curve can turn around at a high-latitude horizon
|
|
// fold. Each monotonic branch is emitted separately; descending branches are
|
|
// reversed so their geometry is retained without violating the API contract.
|
|
func splitSolarEclipseRiseSetTimeFolds(segment []SolarEclipsePathPoint) [][]SolarEclipsePathPoint {
|
|
if len(segment) < 2 {
|
|
return nil
|
|
}
|
|
points := make([]SolarEclipsePathPoint, 0, len(segment))
|
|
for _, point := range segment {
|
|
if len(points) > 0 {
|
|
last := points[len(points)-1]
|
|
if math.Abs(point.JDE-last.JDE) <= solarEclipseRiseSetTimeEpsilonDays {
|
|
if solarEclipsePathDistanceKM(point, last) <= 0.01 {
|
|
continue
|
|
}
|
|
// Distinct points at the same instant are a junction, not a
|
|
// valid edge of a timed segment. Keep both as separate runs.
|
|
points = append(points, point)
|
|
continue
|
|
}
|
|
}
|
|
points = append(points, point)
|
|
}
|
|
if len(points) < 2 {
|
|
return nil
|
|
}
|
|
result := make([][]SolarEclipsePathPoint, 0, 2)
|
|
start := 0
|
|
direction := 0
|
|
flush := func(end int, branchDirection int) {
|
|
if end-start < 1 {
|
|
return
|
|
}
|
|
branch := append([]SolarEclipsePathPoint(nil), points[start:end+1]...)
|
|
if branchDirection < 0 {
|
|
for left, right := 0, len(branch)-1; left < right; left, right = left+1, right-1 {
|
|
branch[left], branch[right] = branch[right], branch[left]
|
|
}
|
|
}
|
|
if len(branch) >= 2 && branch[len(branch)-1].JDE > branch[0].JDE+solarEclipseRiseSetTimeEpsilonDays {
|
|
result = append(result, branch)
|
|
}
|
|
}
|
|
for index := 1; index < len(points); index++ {
|
|
delta := points[index].JDE - points[index-1].JDE
|
|
if math.Abs(delta) <= solarEclipseRiseSetTimeEpsilonDays {
|
|
flush(index-1, direction)
|
|
start = index
|
|
direction = 0
|
|
continue
|
|
}
|
|
sign := 1
|
|
if delta < 0 {
|
|
sign = -1
|
|
}
|
|
if direction == 0 {
|
|
direction = sign
|
|
continue
|
|
}
|
|
if sign != direction {
|
|
// Keep the fold vertex in both adjacent branches. This is
|
|
// necessary to retain the actual spatial turn after reversing
|
|
// the descending branch.
|
|
flush(index-1, direction)
|
|
start = index - 1
|
|
direction = sign
|
|
}
|
|
}
|
|
flush(len(points)-1, direction)
|
|
return result
|
|
}
|
|
|
|
func (solver solarEclipseSolver) completeRiseSetFoldEndpoints(
|
|
curve *SolarEclipseRiseSetCurve,
|
|
stepDays float64,
|
|
) {
|
|
if curve == nil || len(curve.Segments) < 2 {
|
|
return
|
|
}
|
|
type endpointRef struct {
|
|
segmentIndex int
|
|
atStart bool
|
|
point SolarEclipsePathPoint
|
|
}
|
|
endpoints := make([]endpointRef, 0, 2*len(curve.Segments))
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
endpoints = append(endpoints, endpointRef{
|
|
segmentIndex: segmentIndex,
|
|
atStart: atStart,
|
|
point: solarEclipseRiseSetSegmentEndpoint(segment, atStart),
|
|
})
|
|
}
|
|
}
|
|
used := make(map[[2]int]bool, len(endpoints))
|
|
for firstIndex := 0; firstIndex < len(endpoints); firstIndex++ {
|
|
first := endpoints[firstIndex]
|
|
if used[[2]int{first.segmentIndex, boolInt(first.atStart)}] {
|
|
continue
|
|
}
|
|
for secondIndex := firstIndex + 1; secondIndex < len(endpoints); secondIndex++ {
|
|
second := endpoints[secondIndex]
|
|
if first.segmentIndex == second.segmentIndex || first.atStart != second.atStart ||
|
|
used[[2]int{second.segmentIndex, boolInt(second.atStart)}] {
|
|
continue
|
|
}
|
|
if math.Abs(first.point.JDE-second.point.JDE) > math.Max(1e-8, stepDays/4) {
|
|
continue
|
|
}
|
|
distance := solarEclipsePathDistanceKM(first.point, second.point)
|
|
if distance <= 0.01 || distance > 6000 {
|
|
continue
|
|
}
|
|
fold, ok := solver.refineRiseSetFold(first.point, second.point, curve.Phase == RiseSetPhaseGreatest)
|
|
if !ok || math.Abs(fold.JDE-(first.point.JDE+second.point.JDE)/2) > 2.5*stepDays ||
|
|
solarEclipsePathDistanceKM(fold, first.point) > 6000 || solarEclipsePathDistanceKM(fold, second.point) > 6000 {
|
|
continue
|
|
}
|
|
evaluation := solver.magnitudeEvaluationAt(fold.JDE)
|
|
_, key, valid := evaluation.classify(fold.Longitude, fold.Latitude, curve.Phase == RiseSetPhaseGreatest)
|
|
if !valid || key.phase != curve.Phase || key.direction != curve.Direction {
|
|
continue
|
|
}
|
|
curve.Segments[first.segmentIndex] = solarEclipseRiseSetAddEndpoint(
|
|
curve.Segments[first.segmentIndex], fold, first.atStart,
|
|
)
|
|
curve.Segments[second.segmentIndex] = solarEclipseRiseSetAddEndpoint(
|
|
curve.Segments[second.segmentIndex], fold, second.atStart,
|
|
)
|
|
used[[2]int{first.segmentIndex, boolInt(first.atStart)}] = true
|
|
used[[2]int{second.segmentIndex, boolInt(second.atStart)}] = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func boolInt(value bool) int {
|
|
if value {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (solver solarEclipseSolver) completeRiseSetPhaseJunctions(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
stepDays float64,
|
|
phaseJunctions []solarEclipseRiseSetPhaseJunction,
|
|
) {
|
|
curveIndices := make(map[solarEclipseRiseSetCurveKey]int, len(curves))
|
|
for index, curve := range curves {
|
|
curveIndices[solarEclipseRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
|
|
}
|
|
for _, direction := range []RiseSetDirection{RiseSetDirectionRise, RiseSetDirectionSet} {
|
|
startIndex, haveStart := curveIndices[solarEclipseRiseSetCurveKey{phase: RiseSetPhaseStart, direction: direction}]
|
|
greatestIndex, haveGreatest := curveIndices[solarEclipseRiseSetCurveKey{phase: RiseSetPhaseGreatest, direction: direction}]
|
|
endIndex, haveEnd := curveIndices[solarEclipseRiseSetCurveKey{phase: RiseSetPhaseEnd, direction: direction}]
|
|
if !haveStart || !haveGreatest || !haveEnd {
|
|
continue
|
|
}
|
|
for _, candidate := range phaseJunctions {
|
|
if candidate.direction != direction {
|
|
continue
|
|
}
|
|
solver.attachRiseSetPhaseJunction(
|
|
curves, startIndex, greatestIndex, endIndex,
|
|
candidate.point, direction, stepDays,
|
|
)
|
|
}
|
|
seeds := solarEclipseRiseSetUnsharedEndpoints(startIndex, curves[startIndex].Segments)
|
|
for _, seed := range seeds {
|
|
candidateSeeds := []SolarEclipsePathPoint{seed.point}
|
|
if endEndpoint, endOK := solarEclipseClosestRiseSetEndpoint(
|
|
seed.point, endIndex, curves[endIndex].Segments, nil, stepDays,
|
|
); endOK {
|
|
candidateSeeds = append([]SolarEclipsePathPoint{
|
|
solarEclipseRiseSetMidpoint(seed.point, endEndpoint.point),
|
|
endEndpoint.point,
|
|
}, candidateSeeds...)
|
|
}
|
|
junction, ok := SolarEclipsePathPoint{}, false
|
|
for _, candidate := range candidateSeeds {
|
|
junction, ok = solver.refineRiseSetPhaseJunctionOnHorizon(candidate)
|
|
if !ok {
|
|
junction, ok = solver.refineRiseSetPhaseJunction(
|
|
candidate.JDE, candidate.Longitude, candidate.Latitude,
|
|
)
|
|
}
|
|
if ok {
|
|
break
|
|
}
|
|
}
|
|
if !ok || math.Abs(junction.JDE-seed.point.JDE) > 3*stepDays ||
|
|
solarEclipsePathDistanceKM(junction, seed.point) > 3000 {
|
|
continue
|
|
}
|
|
solver.attachRiseSetPhaseJunction(
|
|
curves, startIndex, greatestIndex, endIndex,
|
|
junction, direction, stepDays,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (solver solarEclipseSolver) attachRiseSetPhaseJunction(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
startIndex, greatestIndex, endIndex int,
|
|
junction SolarEclipsePathPoint,
|
|
direction RiseSetDirection,
|
|
stepDays float64,
|
|
) bool {
|
|
matched := make([]solarEclipseRiseSetPhaseAttachment, 0, 3)
|
|
for _, curveIndex := range []int{startIndex, greatestIndex, endIndex} {
|
|
endpoint, ok := solarEclipseClosestRiseSetEndpoint(
|
|
junction, curveIndex, curves[curveIndex].Segments, nil, stepDays,
|
|
)
|
|
if !ok {
|
|
endpoint, ok = solarEclipseClosestRiseSetFoldEndpoint(
|
|
junction, curveIndex, curves[curveIndex].Segments, stepDays,
|
|
)
|
|
}
|
|
if !ok {
|
|
return false
|
|
}
|
|
phase := curves[curveIndex].Phase
|
|
attachment := solarEclipseRiseSetPhaseAttachment{endpoint: endpoint}
|
|
if !solarEclipseRiseSetEndpointTimeDirectionValid(junction, endpoint) ||
|
|
(solarEclipsePathDistanceKM(junction, endpoint.point) > solarEclipseRiseSetPhaseConnectionLimitKM &&
|
|
!solver.riseSetPhaseSegmentIsContinuous(junction, endpoint.point, phase, direction)) {
|
|
greatest := phase == RiseSetPhaseGreatest
|
|
fold, foldOK := solver.refineRiseSetFold(junction, endpoint.point, greatest)
|
|
if !foldOK || !solver.riseSetFoldBridgesPhaseJunction(
|
|
junction, endpoint, fold, phase, direction,
|
|
) {
|
|
fold, foldOK = solver.refineRiseSetFoldNearPhaseJunction(
|
|
junction, endpoint, phase, direction, stepDays,
|
|
)
|
|
}
|
|
if !foldOK || !solver.riseSetFoldBridgesPhaseJunction(
|
|
junction, endpoint, fold, phase, direction,
|
|
) {
|
|
return false
|
|
}
|
|
attachment.fold, attachment.hasFold = fold, true
|
|
}
|
|
matched = append(matched, attachment)
|
|
}
|
|
for _, attachment := range matched {
|
|
endpoint := attachment.endpoint
|
|
if attachment.hasFold {
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex] = solarEclipseRiseSetAddEndpoint(
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex], attachment.fold, endpoint.atStart,
|
|
)
|
|
shortBranch := []SolarEclipsePathPoint{attachment.fold, junction}
|
|
if !endpoint.atStart {
|
|
shortBranch[0], shortBranch[1] = shortBranch[1], shortBranch[0]
|
|
}
|
|
curves[endpoint.curveIndex].Segments = append(curves[endpoint.curveIndex].Segments, shortBranch)
|
|
continue
|
|
}
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex] = solarEclipseRiseSetAddEndpoint(
|
|
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex], junction, endpoint.atStart,
|
|
)
|
|
}
|
|
return true
|
|
}
|
|
|
|
type solarEclipseRiseSetEndpointRef struct {
|
|
curveIndex int
|
|
segmentIndex int
|
|
atStart bool
|
|
point SolarEclipsePathPoint
|
|
}
|
|
|
|
type solarEclipseRiseSetPhaseAttachment struct {
|
|
endpoint solarEclipseRiseSetEndpointRef
|
|
fold SolarEclipsePathPoint
|
|
hasFold bool
|
|
}
|
|
|
|
func solarEclipseRiseSetMidpoint(first, second SolarEclipsePathPoint) SolarEclipsePathPoint {
|
|
return SolarEclipsePathPoint{
|
|
JDE: (first.JDE + second.JDE) / 2,
|
|
Longitude: normalizeLongitude(
|
|
first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2,
|
|
),
|
|
Latitude: (first.Latitude + second.Latitude) / 2,
|
|
SunAltitude: (first.SunAltitude + second.SunAltitude) / 2,
|
|
}
|
|
}
|
|
|
|
func solarEclipseRiseSetUnsharedEndpoints(
|
|
curveIndex int,
|
|
segments [][]SolarEclipsePathPoint,
|
|
) []solarEclipseRiseSetEndpointRef {
|
|
endpoints := make([]solarEclipseRiseSetEndpointRef, 0, 2*len(segments))
|
|
for segmentIndex, segment := range segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
endpoints = append(endpoints, solarEclipseRiseSetEndpointRef{
|
|
curveIndex: curveIndex, segmentIndex: segmentIndex, atStart: atStart,
|
|
point: solarEclipseRiseSetSegmentEndpoint(segment, atStart),
|
|
})
|
|
}
|
|
}
|
|
result := make([]solarEclipseRiseSetEndpointRef, 0, len(endpoints))
|
|
for index, endpoint := range endpoints {
|
|
shared := false
|
|
for otherIndex, other := range endpoints {
|
|
if index == otherIndex || endpoint.segmentIndex == other.segmentIndex {
|
|
continue
|
|
}
|
|
if math.Abs(endpoint.point.JDE-other.point.JDE) <= 1e-8 &&
|
|
solarEclipsePathDistanceKM(endpoint.point, other.point) <= 0.01 {
|
|
shared = true
|
|
break
|
|
}
|
|
}
|
|
if !shared {
|
|
result = append(result, endpoint)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func solarEclipseClosestRiseSetEndpoint(
|
|
junction SolarEclipsePathPoint,
|
|
curveIndex int,
|
|
segments [][]SolarEclipsePathPoint,
|
|
used map[[3]int]bool,
|
|
stepDays float64,
|
|
) (solarEclipseRiseSetEndpointRef, bool) {
|
|
best := solarEclipseRiseSetEndpointRef{}
|
|
bestMetric := math.Inf(1)
|
|
for segmentIndex, segment := range segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
side := 1
|
|
if atStart {
|
|
side = 0
|
|
}
|
|
if used[[3]int{curveIndex, segmentIndex, side}] {
|
|
continue
|
|
}
|
|
point := solarEclipseRiseSetSegmentEndpoint(segment, atStart)
|
|
if atStart && junction.JDE > point.JDE+solarEclipseRiseSetAttachmentTimeToleranceDays ||
|
|
!atStart && junction.JDE < point.JDE-solarEclipseRiseSetAttachmentTimeToleranceDays ||
|
|
math.Abs(junction.JDE-point.JDE) > 3*stepDays {
|
|
continue
|
|
}
|
|
metric := solarEclipsePathDistanceKM(junction, point) + math.Abs(junction.JDE-point.JDE)*8640
|
|
if metric < bestMetric {
|
|
best = solarEclipseRiseSetEndpointRef{
|
|
curveIndex: curveIndex, segmentIndex: segmentIndex, atStart: atStart, point: point,
|
|
}
|
|
bestMetric = metric
|
|
}
|
|
}
|
|
}
|
|
return best, bestMetric < math.Inf(1)
|
|
}
|
|
|
|
func solarEclipseClosestRiseSetFoldEndpoint(
|
|
junction SolarEclipsePathPoint,
|
|
curveIndex int,
|
|
segments [][]SolarEclipsePathPoint,
|
|
stepDays float64,
|
|
) (solarEclipseRiseSetEndpointRef, bool) {
|
|
best := solarEclipseRiseSetEndpointRef{}
|
|
bestMetric := math.Inf(1)
|
|
for segmentIndex, segment := range segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
point := solarEclipseRiseSetSegmentEndpoint(segment, atStart)
|
|
deltaDays := math.Abs(junction.JDE - point.JDE)
|
|
distance := solarEclipsePathDistanceKM(junction, point)
|
|
if deltaDays > 3*stepDays || distance > 6000 {
|
|
continue
|
|
}
|
|
metric := distance + deltaDays*8640
|
|
if metric < bestMetric {
|
|
best = solarEclipseRiseSetEndpointRef{
|
|
curveIndex: curveIndex, segmentIndex: segmentIndex, atStart: atStart, point: point,
|
|
}
|
|
bestMetric = metric
|
|
}
|
|
}
|
|
}
|
|
return best, bestMetric < math.Inf(1)
|
|
}
|
|
|
|
func (solver solarEclipseSolver) completeRiseSetDirectionJunctions(
|
|
curves []SolarEclipseRiseSetCurve,
|
|
stepDays float64,
|
|
) {
|
|
curveIndices := make(map[solarEclipseRiseSetCurveKey]int, len(curves))
|
|
for index, curve := range curves {
|
|
curveIndices[solarEclipseRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
|
|
}
|
|
for _, phase := range []RiseSetPhase{RiseSetPhaseStart, RiseSetPhaseGreatest, RiseSetPhaseEnd} {
|
|
riseIndex, haveRise := curveIndices[solarEclipseRiseSetCurveKey{phase: phase, direction: RiseSetDirectionRise}]
|
|
setIndex, haveSet := curveIndices[solarEclipseRiseSetCurveKey{phase: phase, direction: RiseSetDirectionSet}]
|
|
if !haveRise || !haveSet {
|
|
continue
|
|
}
|
|
riseEndpoints := solarEclipseRiseSetAllEndpoints(riseIndex, curves[riseIndex].Segments)
|
|
setEndpoints := solarEclipseRiseSetAllEndpoints(setIndex, curves[setIndex].Segments)
|
|
usedRise := make([]bool, len(riseEndpoints))
|
|
usedSet := make([]bool, len(setEndpoints))
|
|
for riseEndpointIndex, riseEndpoint := range riseEndpoints {
|
|
bestSetIndex := -1
|
|
bestMetric := math.Inf(1)
|
|
for setEndpointIndex, setEndpoint := range setEndpoints {
|
|
if usedSet[setEndpointIndex] || math.Abs(riseEndpoint.point.JDE-setEndpoint.point.JDE) > 3*stepDays {
|
|
continue
|
|
}
|
|
distance := solarEclipsePathDistanceKM(riseEndpoint.point, setEndpoint.point)
|
|
if distance > 2500 {
|
|
continue
|
|
}
|
|
metric := distance + math.Abs(riseEndpoint.point.JDE-setEndpoint.point.JDE)*8640
|
|
if metric < bestMetric {
|
|
bestSetIndex, bestMetric = setEndpointIndex, metric
|
|
}
|
|
}
|
|
if bestSetIndex < 0 {
|
|
continue
|
|
}
|
|
setEndpoint := setEndpoints[bestSetIndex]
|
|
longitude := normalizeLongitude(
|
|
riseEndpoint.point.Longitude + math.Remainder(setEndpoint.point.Longitude-riseEndpoint.point.Longitude, 360)/2,
|
|
)
|
|
latitude := (riseEndpoint.point.Latitude + setEndpoint.point.Latitude) / 2
|
|
junction, ok := solver.refineRiseSetDirectionJunction(
|
|
(riseEndpoint.point.JDE+setEndpoint.point.JDE)/2, longitude, latitude, phase == RiseSetPhaseGreatest,
|
|
)
|
|
if !ok || math.Abs(junction.JDE-riseEndpoint.point.JDE) > 3*stepDays ||
|
|
math.Abs(junction.JDE-setEndpoint.point.JDE) > 3*stepDays ||
|
|
solarEclipsePathDistanceKM(junction, riseEndpoint.point) > 2500 ||
|
|
solarEclipsePathDistanceKM(junction, setEndpoint.point) > 2500 {
|
|
continue
|
|
}
|
|
if !solarEclipseRiseSetEndpointTimeDirectionValid(junction, riseEndpoint) ||
|
|
!solarEclipseRiseSetEndpointTimeDirectionValid(junction, setEndpoint) {
|
|
continue
|
|
}
|
|
curves[riseIndex].Segments[riseEndpoint.segmentIndex] = solarEclipseRiseSetAddEndpoint(
|
|
curves[riseIndex].Segments[riseEndpoint.segmentIndex], junction, riseEndpoint.atStart,
|
|
)
|
|
curves[setIndex].Segments[setEndpoint.segmentIndex] = solarEclipseRiseSetAddEndpoint(
|
|
curves[setIndex].Segments[setEndpoint.segmentIndex], junction, setEndpoint.atStart,
|
|
)
|
|
usedRise[riseEndpointIndex] = true
|
|
usedSet[bestSetIndex] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func solarEclipseRiseSetAllEndpoints(
|
|
curveIndex int,
|
|
segments [][]SolarEclipsePathPoint,
|
|
) []solarEclipseRiseSetEndpointRef {
|
|
endpoints := make([]solarEclipseRiseSetEndpointRef, 0, 2*len(segments))
|
|
for segmentIndex, segment := range segments {
|
|
if len(segment) == 0 {
|
|
continue
|
|
}
|
|
for _, atStart := range []bool{true, false} {
|
|
endpoints = append(endpoints, solarEclipseRiseSetEndpointRef{
|
|
curveIndex: curveIndex, segmentIndex: segmentIndex, atStart: atStart,
|
|
point: solarEclipseRiseSetSegmentEndpoint(segment, atStart),
|
|
})
|
|
}
|
|
}
|
|
return endpoints
|
|
}
|
|
|
|
func solarEclipseRiseSetEndpointTimeDirectionValid(
|
|
junction SolarEclipsePathPoint,
|
|
endpoint solarEclipseRiseSetEndpointRef,
|
|
) bool {
|
|
if endpoint.atStart {
|
|
return junction.JDE <= endpoint.point.JDE+solarEclipseRiseSetAttachmentTimeToleranceDays
|
|
}
|
|
return junction.JDE >= endpoint.point.JDE-solarEclipseRiseSetAttachmentTimeToleranceDays
|
|
}
|
|
|
|
func (solver solarEclipseSolver) refineRiseSetDirectionJunction(
|
|
jd, longitude, latitude float64,
|
|
greatest bool,
|
|
) (SolarEclipsePathPoint, bool) {
|
|
const (
|
|
geographicStep = 1e-4
|
|
timeStep = 1.0 / 86400.0
|
|
)
|
|
for iteration := 0; iteration < 24; iteration++ {
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
|
residual, ok := solarEclipseRiseSetDirectionJunctionResidualAt(evaluation, longitude, latitude, greatest)
|
|
if !ok {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
if math.Abs(residual[0]) <= 1e-10 && math.Abs(residual[1]) <= 1e-10 && math.Abs(residual[2]) <= 1e-10 {
|
|
break
|
|
}
|
|
longitudeResidual, longitudeOK := solarEclipseRiseSetDirectionJunctionResidualAt(
|
|
evaluation, longitude+geographicStep, latitude, greatest,
|
|
)
|
|
latitudeResidual, latitudeOK := solarEclipseRiseSetDirectionJunctionResidualAt(
|
|
evaluation, longitude, latitude+geographicStep, greatest,
|
|
)
|
|
timeResidual, timeOK := solver.riseSetDirectionJunctionResidual(
|
|
jd+timeStep, longitude, latitude, greatest,
|
|
)
|
|
if !longitudeOK || !latitudeOK || !timeOK {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
matrix := [3][3]float64{}
|
|
for row := 0; row < 3; row++ {
|
|
matrix[row][0] = (longitudeResidual[row] - residual[row]) / geographicStep
|
|
matrix[row][1] = (latitudeResidual[row] - residual[row]) / geographicStep
|
|
matrix[row][2] = (timeResidual[row] - residual[row]) / timeStep
|
|
}
|
|
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -residual[2]})
|
|
if !ok {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
geographicScale := math.Max(math.Abs(delta[0]), math.Abs(delta[1]))
|
|
if geographicScale > 2 {
|
|
delta[0] *= 2 / geographicScale
|
|
delta[1] *= 2 / geographicScale
|
|
}
|
|
if math.Abs(delta[2]) > 5.0/1440.0 {
|
|
delta[2] = math.Copysign(5.0/1440.0, delta[2])
|
|
}
|
|
longitude = normalizeLongitude(longitude + delta[0])
|
|
latitude += delta[1]
|
|
jd += delta[2]
|
|
if latitude <= -89.999999 || latitude >= 89.999999 {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
}
|
|
residual, ok := solver.riseSetDirectionJunctionResidual(jd, longitude, latitude, greatest)
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-8 || math.Abs(residual[2]) > 1e-7 {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
if greatest {
|
|
if solarEclipsePartialContactGap(state) > 1e-7 || evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
} else if math.Abs(evaluation.partialContactDerivative(longitude, latitude)) < 1e-10 {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
return SolarEclipsePathPoint{
|
|
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: residual[1] / rad,
|
|
}, true
|
|
}
|
|
|
|
func (solver solarEclipseSolver) riseSetDirectionJunctionResidual(
|
|
jd, longitude, latitude float64,
|
|
greatest bool,
|
|
) ([3]float64, bool) {
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
|
return solarEclipseRiseSetDirectionJunctionResidualAt(evaluation, longitude, latitude, greatest)
|
|
}
|
|
|
|
func solarEclipseRiseSetDirectionJunctionResidualAt(
|
|
evaluation solarEclipseRiseSetEvaluation,
|
|
longitude, latitude float64,
|
|
greatest bool,
|
|
) ([3]float64, bool) {
|
|
phaseResidual, phaseOK := solarEclipseRiseSetPhaseResidual(evaluation, longitude, latitude, greatest)
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
altitudeDerivative := evaluation.sunAltitudeDerivative(longitude, latitude)
|
|
residual := [3]float64{phaseResidual, state.sunAltitudeRad, altitudeDerivative}
|
|
return residual, phaseOK && finite(residual[1]) && finite(residual[2])
|
|
}
|
|
|
|
func solarEclipseRiseSetSegmentEndpoint(segment []SolarEclipsePathPoint, atStart bool) SolarEclipsePathPoint {
|
|
if atStart {
|
|
return segment[0]
|
|
}
|
|
return segment[len(segment)-1]
|
|
}
|
|
|
|
func solarEclipseRiseSetAddEndpoint(
|
|
segment []SolarEclipsePathPoint,
|
|
point SolarEclipsePathPoint,
|
|
atStart bool,
|
|
) []SolarEclipsePathPoint {
|
|
current := solarEclipseRiseSetSegmentEndpoint(segment, atStart)
|
|
if math.Abs(current.JDE-point.JDE) <= solarEclipseRiseSetAttachmentTimeToleranceDays &&
|
|
solarEclipsePathDistanceKM(current, point) <= solarEclipseRiseSetAttachmentDistanceToleranceKM {
|
|
segmentIndex := len(segment) - 1
|
|
if atStart {
|
|
segmentIndex = 0
|
|
}
|
|
segment[segmentIndex] = point
|
|
return segment
|
|
}
|
|
if atStart {
|
|
result := make([]SolarEclipsePathPoint, 0, len(segment)+1)
|
|
result = append(result, point)
|
|
return append(result, segment...)
|
|
}
|
|
return append(segment, point)
|
|
}
|