feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user