feat: 完善时标与天象几何计算并扩展输出接口

- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
This commit is contained in:
2026-09-23 18:55:12 +08:00
parent 1f31a9b5b5
commit 16c62a97d5
503 changed files with 33290 additions and 9471 deletions
+57 -57
View File
@@ -526,10 +526,10 @@ func (solver solarEclipseSolver) validMagnitudeArcState(
magnitude, referenceJDE float64,
iterations int,
) (solarEclipseMagnitudeArcState, int, bool) {
jd := referenceJDE + coordinates[2]/solarEclipseMagnitudeContourTimeScale
jde := referenceJDE + coordinates[2]/solarEclipseMagnitudeContourTimeScale
longitude := normalizeLongitude(coordinates[0])
latitude := coordinates[1]
evaluation := solver.magnitudeEvaluationAt(jd)
evaluation := solver.magnitudeEvaluationAt(jde)
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
if math.Abs(solarEclipseMagnitudeAtTarget(state, magnitude)-magnitude) > 1e-7 ||
evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
@@ -543,7 +543,7 @@ func (solver solarEclipseSolver) validMagnitudeArcState(
coordinates: coordinates,
tangent: tangent,
point: SolarEclipsePathPoint{
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
JDE: jde, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
},
}, iterations, true
}
@@ -552,10 +552,10 @@ func (solver solarEclipseSolver) magnitudeEnvelopeJacobian(
coordinates [3]float64,
magnitude, referenceJDE float64,
) ([2]float64, [2][3]float64, bool) {
jd := referenceJDE + coordinates[2]/solarEclipseMagnitudeContourTimeScale
jde := referenceJDE + coordinates[2]/solarEclipseMagnitudeContourTimeScale
longitude := normalizeLongitude(coordinates[0])
latitude := coordinates[1]
evaluation := solver.magnitudeEvaluationAt(jd)
evaluation := solver.magnitudeEvaluationAt(jde)
residual, ok := solarEclipseMagnitudeEnvelopeResidualAt(evaluation, longitude, latitude, magnitude)
if !ok {
return [2]float64{}, [2][3]float64{}, false
@@ -574,14 +574,14 @@ func (solver solarEclipseSolver) magnitudeEnvelopeJacobian(
jacobian[row][column] = (shiftedResidual[row] - residual[row]) / steps[column]
}
}
timeEvaluation := solver.magnitudeEvaluationAt(jd + steps[2]/solarEclipseMagnitudeContourTimeScale)
timeEvaluation := solver.magnitudeEvaluationAt(jde + steps[2]/solarEclipseMagnitudeContourTimeScale)
timeResidual, timeOK := solarEclipseMagnitudeEnvelopeResidualAt(
timeEvaluation, longitude, latitude, magnitude,
)
if !timeOK {
return [2]float64{}, [2][3]float64{}, false
}
beforeEvaluation := solver.magnitudeEvaluationAt(jd - steps[2]/solarEclipseMagnitudeContourTimeScale)
beforeEvaluation := solver.magnitudeEvaluationAt(jde - steps[2]/solarEclipseMagnitudeContourTimeScale)
beforeResidual, beforeOK := solarEclipseMagnitudeEnvelopeResidualAt(
beforeEvaluation, longitude, latitude, magnitude,
)
@@ -645,14 +645,14 @@ func (solver solarEclipseSolver) refineMagnitudeHorizonCrossing(
}
func (solver solarEclipseSolver) refineMagnitudeHorizonPoint(
jd, longitude, latitude, magnitude float64,
jde, longitude, latitude, magnitude float64,
) (SolarEclipsePathPoint, bool) {
const (
geographicStep = 1e-4
timeStep = 5.0 / 86400.0
)
for iteration := 0; iteration < 24; iteration++ {
evaluation := solver.magnitudeEvaluationAt(jd)
evaluation := solver.magnitudeEvaluationAt(jde)
residual, ok := solarEclipseMagnitudeHorizonResidualAt(evaluation, longitude, latitude, magnitude)
if !ok {
return SolarEclipsePathPoint{}, false
@@ -666,7 +666,7 @@ func (solver solarEclipseSolver) refineMagnitudeHorizonPoint(
latitudeResidual, latOK := solarEclipseMagnitudeHorizonResidualAt(
evaluation, longitude, latitude+geographicStep, magnitude,
)
timeResidual, timeOK := solver.magnitudeHorizonResidual(jd+timeStep, longitude, latitude, magnitude)
timeResidual, timeOK := solver.magnitudeHorizonResidual(jde+timeStep, longitude, latitude, magnitude)
if !lonOK || !latOK || !timeOK {
return SolarEclipsePathPoint{}, false
}
@@ -690,49 +690,49 @@ func (solver solarEclipseSolver) refineMagnitudeHorizonPoint(
}
longitude = normalizeLongitude(longitude + delta[0])
latitude += delta[1]
jd += delta[2]
jde += delta[2]
}
residual, ok := solver.magnitudeHorizonResidual(jd, longitude, latitude, magnitude)
residual, ok := solver.magnitudeHorizonResidual(jde, longitude, latitude, magnitude)
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)
evaluation := solver.magnitudeEvaluationAt(jde)
if evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
return SolarEclipsePathPoint{}, false
}
return SolarEclipsePathPoint{
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: residual[2] / rad,
JDE: jde, Longitude: longitude, Latitude: latitude, SunAltitude: residual[2] / rad,
}, true
}
func (solver solarEclipseSolver) magnitudeEvaluationAt(jd float64) solarEclipseRiseSetEvaluation {
func (solver solarEclipseSolver) magnitudeEvaluationAt(jde float64) solarEclipseRiseSetEvaluation {
return solarEclipseRiseSetEvaluation{
jd: jd,
center: solver.localStateContextAt(jd),
before: solver.localStateContextAt(jd - solarEclipseRiseSetDerivativeStepDays),
after: solver.localStateContextAt(jd + solarEclipseRiseSetDerivativeStepDays),
jd: jde,
center: solver.localStateContextAt(jde),
before: solver.localStateContextAt(jde - solarEclipseRiseSetDerivativeStepDays),
after: solver.localStateContextAt(jde + solarEclipseRiseSetDerivativeStepDays),
}
}
func (solver solarEclipseSolver) magnitudeCandidateEvaluationAt(jd float64) solarEclipseRiseSetEvaluation {
func (solver solarEclipseSolver) magnitudeCandidateEvaluationAt(jde float64) solarEclipseRiseSetEvaluation {
return solarEclipseRiseSetEvaluation{
jd: jd,
center: solver.localStateContextCandidateAt(jd),
before: solver.localStateContextCandidateAt(jd - solarEclipseRiseSetDerivativeStepDays),
after: solver.localStateContextCandidateAt(jd + solarEclipseRiseSetDerivativeStepDays),
jd: jde,
center: solver.localStateContextCandidateAt(jde),
before: solver.localStateContextCandidateAt(jde - solarEclipseRiseSetDerivativeStepDays),
after: solver.localStateContextCandidateAt(jde + solarEclipseRiseSetDerivativeStepDays),
}
}
func (solver solarEclipseSolver) localStateContextAt(jd float64) localSolarEclipseStateContext {
func (solver solarEclipseSolver) localStateContextAt(jde float64) localSolarEclipseStateContext {
if solver.localStateContextCache == nil {
return newLocalSolarEclipseStateContextWithOverride(jd, solver.deltaTSeconds, solver.params)
return newLocalSolarEclipseStateContextWithOverride(jde, solver.deltaTSeconds, solver.params)
}
key := math.Float64bits(jd)
key := math.Float64bits(jde)
if context, ok := solver.localStateContextCache[key]; ok &&
context.generation == deltaTGenerationValue() {
return context
}
context := newLocalSolarEclipseStateContextWithOverride(jd, solver.deltaTSeconds, solver.params)
context := newLocalSolarEclipseStateContextWithOverride(jde, solver.deltaTSeconds, solver.params)
return storeLocalSolarEclipseStateContext(solver.localStateContextCache, key, context)
}
@@ -755,26 +755,26 @@ func storeLocalSolarEclipseStateContext(
return context
}
func (solver solarEclipseSolver) localStateContextCandidateAt(jd float64) localSolarEclipseStateContext {
func (solver solarEclipseSolver) localStateContextCandidateAt(jde float64) localSolarEclipseStateContext {
if solver.localEphemeris == nil {
return solver.localStateContextAt(jd)
return solver.localStateContextAt(jde)
}
sun, moon, ok := solver.localEphemeris.equatorialAt(jd)
sun, moon, ok := solver.localEphemeris.equatorialAt(jde)
if !ok {
return solver.localStateContextAt(jd)
return solver.localStateContextAt(jde)
}
return localSolarEclipseStateContext{
sunXYZ: solarEclipseLLRToXYZ(sun[0], sun[1], sun[2]),
moonXYZ: solarEclipseLLRToXYZ(moon[0], moon[1], moon[2]),
gst: solver.siderealTimeAt(jd),
gst: solver.siderealTimeAt(jde),
params: solver.params,
}
}
func (solver solarEclipseSolver) magnitudeHorizonResidual(
jd, longitude, latitude, magnitude float64,
jde, longitude, latitude, magnitude float64,
) ([3]float64, bool) {
evaluation := solver.magnitudeEvaluationAt(jd)
evaluation := solver.magnitudeEvaluationAt(jde)
return solarEclipseMagnitudeHorizonResidualAt(evaluation, longitude, latitude, magnitude)
}
@@ -829,13 +829,13 @@ func solveSolarEclipse3x3(matrix [3][3]float64, right [3]float64) ([3]float64, b
return result, true
}
func (solver solarEclipseSolver) magnitudeContourPointsAt(jd, magnitude float64) []SolarEclipsePathPoint {
moon := solver.besselMoonAt(jd)
axis := solver.besselAxisAt(jd)
evaluation := solver.magnitudeEvaluationAt(jd)
func (solver solarEclipseSolver) magnitudeContourPointsAt(jde, magnitude float64) []SolarEclipsePathPoint {
moon := solver.besselMoonAt(jde)
axis := solver.besselAxisAt(jde)
evaluation := solver.magnitudeEvaluationAt(jde)
valueAt := func(angle float64) (float64, bool) {
point, ok := solver.magnitudeContourPointAt(
jd, moon, axis, math.Cos(angle), math.Sin(angle), magnitude,
jde, moon, axis, math.Cos(angle), math.Sin(angle), magnitude,
)
if !ok {
return 0, false
@@ -845,7 +845,7 @@ func (solver solarEclipseSolver) magnitudeContourPointsAt(jd, magnitude float64)
points := make([]SolarEclipsePathPoint, 0, 2)
for _, angle := range riseSetCyclicRoots(solarEclipseMagnitudeContourBoundaryPoints, valueAt) {
seed, ok := solver.magnitudeContourPointAt(
jd, moon, axis, math.Cos(angle), math.Sin(angle), magnitude,
jde, moon, axis, math.Cos(angle), math.Sin(angle), magnitude,
)
if !ok {
continue
@@ -861,27 +861,27 @@ func (solver solarEclipseSolver) magnitudeContourPointsAt(jd, magnitude float64)
continue
}
point := SolarEclipsePathPoint{
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
JDE: jde, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
}
if !solarEclipseRiseSetPointExists(points, point) {
points = append(points, point)
}
}
if magnitude == 1 && len(points) < 2 {
points = solver.appendMagnitudeOneLimitSeeds(points, jd, evaluation)
points = solver.appendMagnitudeOneLimitSeeds(points, jde, evaluation)
}
if len(points) == 0 {
points = solver.magnitudeContourGeographicSeedsAt(jd, magnitude)
points = solver.magnitudeContourGeographicSeedsAt(jde, magnitude)
}
return points
}
func (solver solarEclipseSolver) appendMagnitudeOneLimitSeeds(
points []SolarEclipsePathPoint,
jd float64,
jde float64,
evaluation solarEclipseRiseSetEvaluation,
) []SolarEclipsePathPoint {
center, ok := solver.centralPathPointAt(jd)
center, ok := solver.centralPathPointAt(jde)
if !ok {
return points
}
@@ -901,7 +901,7 @@ func (solver solarEclipseSolver) appendMagnitudeOneLimitSeeds(
continue
}
candidate := SolarEclipsePathPoint{
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
JDE: jde, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
}
if !solarEclipseRiseSetPointExists(points, candidate) {
points = append(points, candidate)
@@ -916,8 +916,8 @@ func (solver solarEclipseSolver) appendMagnitudeOneLimitSeeds(
// total/annular transition. Solving the local-magnitude envelope from the
// greatest point keeps those contours available without changing the normal
// Bessel path.
func (solver solarEclipseSolver) magnitudeContourGeographicSeedsAt(jd, magnitude float64) []SolarEclipsePathPoint {
center, ok := solver.centralPathPointAt(jd)
func (solver solarEclipseSolver) magnitudeContourGeographicSeedsAt(jde, magnitude float64) []SolarEclipsePathPoint {
center, ok := solver.centralPathPointAt(jde)
if !ok {
// A non-central eclipse has no Earth-intersecting shadow axis, but its
// local maximum still has a well-defined geographic stationary point.
@@ -929,12 +929,12 @@ func (solver solarEclipseSolver) magnitudeContourGeographicSeedsAt(jd, magnitude
return nil
}
center = SolarEclipsePathPoint{
JDE: jd,
JDE: jde,
Longitude: result.GreatestLongitude,
Latitude: result.GreatestLatitude,
}
}
evaluation := solver.magnitudeEvaluationAt(jd)
evaluation := solver.magnitudeEvaluationAt(jde)
centerState := evaluation.center.stateAt(center.Longitude*rad, center.Latitude*rad, 0)
maximum := solarEclipseMagnitudeAtTarget(centerState, magnitude)
if !finite(maximum) || maximum <= magnitude+1e-9 {
@@ -1020,7 +1020,7 @@ func (solver solarEclipseSolver) magnitudeContourGeographicSeedsAt(jd, magnitude
if refined {
state := evaluation.center.stateAt(seedLongitude*rad, seedLatitude*rad, 0)
candidate := SolarEclipsePathPoint{
JDE: jd, Longitude: seedLongitude, Latitude: seedLatitude,
JDE: jde, Longitude: seedLongitude, Latitude: seedLatitude,
SunAltitude: state.sunAltitudeRad / rad,
}
if !solarEclipseRiseSetPointExists(seeds, candidate) {
@@ -1103,14 +1103,14 @@ func solarEclipseMagnitudeAtTarget(state localSolarEclipseState, target float64)
}
func (solver solarEclipseSolver) magnitudeContourPointAt(
jd float64,
jde float64,
moon [3]float64,
axis solarEclipseAxis,
directionX, directionY, magnitude float64,
) (SolarEclipsePathPoint, bool) {
if magnitude == 0 {
_, _, sun := solver.besselGeometryAt(jd)
return solver.shadowFootprintPointAt(jd, moon, axis, sun, math.Atan2(directionY, directionX), solarEclipsePenumbralShadow)
_, _, sun := solver.besselGeometryAt(jde)
return solver.shadowFootprintPointAt(jde, moon, axis, sun, math.Atan2(directionY, directionX), solarEclipsePenumbralShadow)
}
radii := solver.shadowRadiiAt(moon[2])
radius := solarEclipseMagnitudeContourRadius(radii, magnitude)
@@ -1160,9 +1160,9 @@ func (solver solarEclipseSolver) magnitudeContourPointAt(
return SolarEclipsePathPoint{}, false
}
longitude, latitude := solarEclipseIntersectionGeodetic(intersection, axis)
sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jd, longitude, latitude, axis.gst)
sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jde, longitude, latitude, axis.gst)
return SolarEclipsePathPoint{
JDE: jd,
JDE: jde,
Longitude: longitude,
Latitude: latitude,
SunAltitude: sunAltitudeRad / rad,