282 lines
9.7 KiB
Go
282 lines
9.7 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
)
|
||
|
|
|
||
|
|
// RiseSetPhase 标识升落边界对应的局部事件阶段。
|
||
|
|
// RiseSetPhase identifies the local event phase represented by a horizon curve.
|
||
|
|
type RiseSetPhase string
|
||
|
|
|
||
|
|
// 升落边界上局部事件的三个阶段取值 / the three local event phase values on a rise/set boundary.
|
||
|
|
const (
|
||
|
|
RiseSetPhaseStart RiseSetPhase = "start"
|
||
|
|
RiseSetPhaseGreatest RiseSetPhase = "greatest"
|
||
|
|
RiseSetPhaseEnd RiseSetPhase = "end"
|
||
|
|
)
|
||
|
|
|
||
|
|
// RiseSetDirection 标识边界上的目标天体正在升起还是落下。
|
||
|
|
// RiseSetDirection identifies whether the occulted body is rising or setting.
|
||
|
|
type RiseSetDirection string
|
||
|
|
|
||
|
|
// 边界上目标天体正在升起或落下 / whether the body is rising or setting along the boundary.
|
||
|
|
const (
|
||
|
|
RiseSetDirectionRise RiseSetDirection = "rise"
|
||
|
|
RiseSetDirectionSet RiseSetDirection = "set"
|
||
|
|
)
|
||
|
|
|
||
|
|
type riseSetCyclicValueFunc func(float64) (float64, bool)
|
||
|
|
|
||
|
|
// 折点处升落残差与零相切而不变号,纯符号扫描会整圈找不到根;极区相位曲线正是在
|
||
|
|
// 这里断开。补根由调用方只在“分支内部空洞”上启用,因此不会改变分支端点语义。
|
||
|
|
// A rise/set fold makes the residual touch zero without changing sign, so a pure
|
||
|
|
// sign scan can return no root at all and a polar phase curve breaks apart there.
|
||
|
|
// The caller enables fold recovery only for interior branch holes, so branch
|
||
|
|
// endpoints keep their existing semantics.
|
||
|
|
const riseSetFoldRootResidualToleranceDeg = 5e-4
|
||
|
|
|
||
|
|
func riseSetCyclicRoots(samples int, valueAt riseSetCyclicValueFunc) []float64 {
|
||
|
|
return riseSetSignChangeRoots(samples, valueAt)
|
||
|
|
}
|
||
|
|
|
||
|
|
// riseSetCyclicRootsWithFoldTolerance 在符号扫描为空时补出相切折点根。
|
||
|
|
// foldTolerance 非正时与历史符号扫描完全一致;正值为折点根的残差极小值上限。
|
||
|
|
// riseSetCyclicRootsWithFoldTolerance falls back to tangent fold roots when the
|
||
|
|
// sign scan stays empty. A non-positive foldTolerance reproduces the historical
|
||
|
|
// sign scan exactly; a positive value bounds the residual minimum accepted as a
|
||
|
|
// fold root.
|
||
|
|
func riseSetCyclicRootsWithFoldTolerance(samples int, foldTolerance float64, valueAt riseSetCyclicValueFunc) []float64 {
|
||
|
|
roots := riseSetSignChangeRoots(samples, valueAt)
|
||
|
|
if len(roots) > 0 || foldTolerance <= 0 {
|
||
|
|
return roots
|
||
|
|
}
|
||
|
|
return riseSetFoldRoots(samples, foldTolerance, valueAt)
|
||
|
|
}
|
||
|
|
|
||
|
|
// riseSetFoldRoots 取相邻采样 |残差| 的严格极小值为候选,由黄金分割核对区间极小值
|
||
|
|
// 是否进入折点容差。
|
||
|
|
// riseSetFoldRoots takes adjacent |residual| samples forming a strict local minimum
|
||
|
|
// as candidates and lets the golden-section minimum decide whether the fold
|
||
|
|
// tolerance is met.
|
||
|
|
func riseSetFoldRoots(samples int, foldTolerance float64, valueAt riseSetCyclicValueFunc) []float64 {
|
||
|
|
if samples < 12 {
|
||
|
|
samples = 12
|
||
|
|
}
|
||
|
|
step := 2 * math.Pi / float64(samples)
|
||
|
|
values := make([]float64, samples)
|
||
|
|
valid := make([]bool, samples)
|
||
|
|
for index := range values {
|
||
|
|
values[index], valid[index] = valueAt(step * float64(index))
|
||
|
|
valid[index] = valid[index] && finite(values[index])
|
||
|
|
}
|
||
|
|
roots := make([]float64, 0, 2)
|
||
|
|
for index := range values {
|
||
|
|
next := (index + 1) % samples
|
||
|
|
previous := (index - 1 + samples) % samples
|
||
|
|
following := (next + 1) % samples
|
||
|
|
if !valid[index] || !valid[next] || !valid[previous] || !valid[following] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
leftValue, rightValue := math.Abs(values[index]), math.Abs(values[next])
|
||
|
|
if math.Abs(values[previous]) <= leftValue || math.Abs(values[following]) <= rightValue {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if angle, ok := riseSetFoldRoot(step*float64(index), step*float64(next), foldTolerance, valueAt); ok {
|
||
|
|
roots = append(roots, riseSetNormalizeRadians(angle))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.Float64s(roots)
|
||
|
|
return roots
|
||
|
|
}
|
||
|
|
|
||
|
|
func riseSetSignChangeRoots(samples int, valueAt riseSetCyclicValueFunc) []float64 {
|
||
|
|
if samples < 12 {
|
||
|
|
samples = 12
|
||
|
|
}
|
||
|
|
step := 2 * math.Pi / float64(samples)
|
||
|
|
values := make([]float64, samples)
|
||
|
|
valid := make([]bool, samples)
|
||
|
|
for index := range values {
|
||
|
|
values[index], valid[index] = valueAt(step * float64(index))
|
||
|
|
valid[index] = valid[index] && finite(values[index])
|
||
|
|
}
|
||
|
|
|
||
|
|
roots := make([]float64, 0, 4)
|
||
|
|
for index := range values {
|
||
|
|
next := (index + 1) % samples
|
||
|
|
if !valid[index] || !valid[next] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
left := step * float64(index)
|
||
|
|
right := step * float64(index+1)
|
||
|
|
leftValue, rightValue := values[index], values[next]
|
||
|
|
if leftValue == 0 {
|
||
|
|
roots = append(roots, riseSetNormalizeRadians(left))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if leftValue*rightValue > 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for iteration := 0; iteration < 48 && right-left > 1e-11; iteration++ {
|
||
|
|
middle := (left + right) / 2
|
||
|
|
middleValue, ok := valueAt(riseSetNormalizeRadians(middle))
|
||
|
|
if !ok || !finite(middleValue) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if leftValue*middleValue <= 0 {
|
||
|
|
right, rightValue = middle, middleValue
|
||
|
|
} else {
|
||
|
|
left, leftValue = middle, middleValue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
roots = append(roots, riseSetNormalizeRadians((left+right)/2))
|
||
|
|
}
|
||
|
|
sort.Float64s(roots)
|
||
|
|
unique := roots[:0]
|
||
|
|
for _, root := range roots {
|
||
|
|
if len(unique) == 0 || riseSetAngularDistance(root, unique[len(unique)-1]) > 1e-7 {
|
||
|
|
unique = append(unique, root)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(unique) > 1 && riseSetAngularDistance(unique[0], unique[len(unique)-1]) <= 1e-7 {
|
||
|
|
unique = unique[:len(unique)-1]
|
||
|
|
}
|
||
|
|
return unique
|
||
|
|
}
|
||
|
|
|
||
|
|
// riseSetFoldRoot 用黄金分割在区间内最小化 |残差|,极小值进入容差时返回折点根。
|
||
|
|
// riseSetFoldRoot minimizes |residual| inside the interval by golden section and
|
||
|
|
// returns the fold root when the minimum stays inside the tolerance.
|
||
|
|
func riseSetFoldRoot(left, right, tolerance float64, valueAt riseSetCyclicValueFunc) (float64, bool) {
|
||
|
|
const goldenRatio = 0.6180339887498949
|
||
|
|
valueAtAbs := func(angle float64) (float64, bool) {
|
||
|
|
value, ok := valueAt(riseSetNormalizeRadians(angle))
|
||
|
|
if !ok || !finite(value) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return math.Abs(value), true
|
||
|
|
}
|
||
|
|
x1 := right - goldenRatio*(right-left)
|
||
|
|
x2 := left + goldenRatio*(right-left)
|
||
|
|
f1, ok1 := valueAtAbs(x1)
|
||
|
|
f2, ok2 := valueAtAbs(x2)
|
||
|
|
if !ok1 || !ok2 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
for iteration := 0; iteration < 48 && right-left > 1e-9; iteration++ {
|
||
|
|
if f1 > f2 {
|
||
|
|
left, x1, f1 = x1, x2, f2
|
||
|
|
x2 = left + goldenRatio*(right-left)
|
||
|
|
if f2, ok2 = valueAtAbs(x2); !ok2 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
right, x2, f2 = x2, x1, f1
|
||
|
|
x1 = right - goldenRatio*(right-left)
|
||
|
|
if f1, ok1 = valueAtAbs(x1); !ok1 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
angle, minimum := (left+right)/2, math.Min(f1, f2)
|
||
|
|
if minimum > tolerance {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return angle, true
|
||
|
|
}
|
||
|
|
|
||
|
|
type riseSetGeographicResidualFunc func(longitude, latitude float64) (float64, float64, bool)
|
||
|
|
|
||
|
|
func riseSetRefineGeographicRoot(
|
||
|
|
longitude, latitude float64,
|
||
|
|
residualAt riseSetGeographicResidualFunc,
|
||
|
|
) (float64, float64, bool) {
|
||
|
|
const finiteDifferenceDegrees = 1e-4
|
||
|
|
for iteration := 0; iteration < 16; iteration++ {
|
||
|
|
first, second, ok := residualAt(longitude, latitude)
|
||
|
|
if !ok || !finite(first) || !finite(second) {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
if math.Abs(first) <= 1e-11 && math.Abs(second) <= 1e-11 {
|
||
|
|
return normalizeLongitude(longitude), latitude, true
|
||
|
|
}
|
||
|
|
firstLon, secondLon, lonOK := residualAt(longitude+finiteDifferenceDegrees, latitude)
|
||
|
|
firstLat, secondLat, latOK := residualAt(longitude, latitude+finiteDifferenceDegrees)
|
||
|
|
if !lonOK || !latOK {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
a := (firstLon - first) / finiteDifferenceDegrees
|
||
|
|
b := (firstLat - first) / finiteDifferenceDegrees
|
||
|
|
c := (secondLon - second) / finiteDifferenceDegrees
|
||
|
|
d := (secondLat - second) / finiteDifferenceDegrees
|
||
|
|
determinant := a*d - b*c
|
||
|
|
if !finite(determinant) || math.Abs(determinant) < 1e-18 {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
deltaLongitude := (-first*d + b*second) / determinant
|
||
|
|
deltaLatitude := (c*first - a*second) / determinant
|
||
|
|
scale := math.Max(math.Abs(deltaLongitude), math.Abs(deltaLatitude))
|
||
|
|
if scale > 5 {
|
||
|
|
deltaLongitude *= 5 / scale
|
||
|
|
deltaLatitude *= 5 / scale
|
||
|
|
}
|
||
|
|
longitude = normalizeLongitude(longitude + deltaLongitude)
|
||
|
|
latitude += deltaLatitude
|
||
|
|
if latitude <= -89.999999 || latitude >= 89.999999 || !finite(latitude) {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
first, second, ok := residualAt(longitude, latitude)
|
||
|
|
return normalizeLongitude(longitude), latitude,
|
||
|
|
ok && finite(first) && finite(second) && math.Abs(first) <= 1e-8 && math.Abs(second) <= 1e-8
|
||
|
|
}
|
||
|
|
|
||
|
|
func riseSetHorizonPoint(centerLongitude, centerLatitude, angle float64) (float64, float64) {
|
||
|
|
longitude := centerLongitude * math.Pi / 180
|
||
|
|
latitude := centerLatitude * math.Pi / 180
|
||
|
|
center := [3]float64{
|
||
|
|
math.Cos(latitude) * math.Cos(longitude),
|
||
|
|
math.Cos(latitude) * math.Sin(longitude),
|
||
|
|
math.Sin(latitude),
|
||
|
|
}
|
||
|
|
reference := [3]float64{0, 0, 1}
|
||
|
|
if math.Abs(center[2]) > 0.9 {
|
||
|
|
reference = [3]float64{1, 0, 0}
|
||
|
|
}
|
||
|
|
first := riseSetUnitVector(riseSetCross(reference, center))
|
||
|
|
second := riseSetUnitVector(riseSetCross(center, first))
|
||
|
|
point := [3]float64{
|
||
|
|
first[0]*math.Cos(angle) + second[0]*math.Sin(angle),
|
||
|
|
first[1]*math.Cos(angle) + second[1]*math.Sin(angle),
|
||
|
|
first[2]*math.Cos(angle) + second[2]*math.Sin(angle),
|
||
|
|
}
|
||
|
|
return normalizeLongitude(math.Atan2(point[1], point[0]) * 180 / math.Pi),
|
||
|
|
math.Asin(math.Max(-1, math.Min(1, point[2]))) * 180 / math.Pi
|
||
|
|
}
|
||
|
|
|
||
|
|
func riseSetCross(first, second [3]float64) [3]float64 {
|
||
|
|
return [3]float64{
|
||
|
|
first[1]*second[2] - first[2]*second[1],
|
||
|
|
first[2]*second[0] - first[0]*second[2],
|
||
|
|
first[0]*second[1] - first[1]*second[0],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func riseSetUnitVector(value [3]float64) [3]float64 {
|
||
|
|
norm := math.Sqrt(value[0]*value[0] + value[1]*value[1] + value[2]*value[2])
|
||
|
|
return [3]float64{value[0] / norm, value[1] / norm, value[2] / norm}
|
||
|
|
}
|
||
|
|
|
||
|
|
func riseSetNormalizeRadians(value float64) float64 {
|
||
|
|
value = math.Mod(value, 2*math.Pi)
|
||
|
|
if value < 0 {
|
||
|
|
value += 2 * math.Pi
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
func riseSetAngularDistance(first, second float64) float64 {
|
||
|
|
return math.Abs(math.Remainder(first-second, 2*math.Pi))
|
||
|
|
}
|