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
+14 -14
View File
@@ -21,7 +21,7 @@ func solarCentralBandSkyOffset(context localSolarEclipseStateContext, longitude,
east := [3]float64{-sun[1] / equatorial, sun[0] / equatorial, 0}
north := [3]float64{-sun[2] * east[1], sun[2] * east[0], equatorial}
radius := math.Asin(solarEclipseEarthEquatorialRadiusKM*context.params.umbralK*localSolarMoonRadiusScale/moonDistance) -
math.Asin(solarEclipseEarthEquatorialRadiusKM*solarEclipseSolarRadiusRatio/sunDistance)
math.Asin(solarEclipseEarthEquatorialRadiusKM*context.params.sunRadiusRatio/sunDistance)
return [3]float64{dotSolarEclipse3(moon, east), dotSolarEclipse3(moon, north), math.Sin(radius)}
}
@@ -85,8 +85,8 @@ func (solver solarEclipseSolver) correctCentralBandVectorBoundary(predictor, tan
}
plane := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
if math.Hypot(residual[0], residual[1]) <= solarEclipseCentralVectorTolerance && math.Abs(plane) <= 1e-9 {
jd := referenceJDE + coordinates[2]/solarEclipseNonCentralBandTimeScale
evaluation := solver.magnitudeEvaluationAt(jd)
jde := referenceJDE + coordinates[2]/solarEclipseNonCentralBandTimeScale
evaluation := solver.magnitudeEvaluationAt(jde)
check, valid := solarCentralBandVectorResidual(evaluation, coordinates[0], coordinates[1], side)
if !valid || math.Hypot(check[0], check[1]) > solarEclipseCentralVectorTolerance {
exact = true
@@ -96,7 +96,7 @@ func (solver solarEclipseSolver) correctCentralBandVectorBoundary(predictor, tan
state := evaluation.center.stateAt(coordinates[0]*rad, coordinates[1]*rad, 0)
return solarEclipseNonCentralBandState{
coordinates: coordinates, tangent: nextTangent,
point: SolarEclipsePathPoint{JDE: jd, Longitude: normalizeLongitude(coordinates[0]), Latitude: coordinates[1], SunAltitude: state.sunAltitudeRad / rad},
point: SolarEclipsePathPoint{JDE: jde, Longitude: normalizeLongitude(coordinates[0]), Latitude: coordinates[1], SunAltitude: state.sunAltitudeRad / rad},
}, valid
}
delta, valid := solveSolarEclipse3x3([3][3]float64{jacobian[0], jacobian[1], tangent}, [3]float64{-residual[0], -residual[1], -plane})
@@ -252,19 +252,19 @@ func (solver solarEclipseSolver) hybridCentralBandTransition(seed SolarEclipsePa
coordinates := [3]float64{seed.Longitude, seed.Latitude, (seed.JDE - referenceJDE) * solarEclipseNonCentralBandTimeScale}
steps := [3]float64{1e-4, 1e-4, solarEclipseNonCentralBandTimeScale / 86400}
for iteration := 0; iteration < 12; iteration++ {
jd := referenceJDE + coordinates[2]/solarEclipseNonCentralBandTimeScale
context := solver.localStateContextAt(jd)
jde := referenceJDE + coordinates[2]/solarEclipseNonCentralBandTimeScale
context := solver.localStateContextAt(jde)
residual := solarCentralBandSkyOffset(context, coordinates[0], coordinates[1])
if math.Hypot(residual[0], residual[1]) < solarEclipseCentralVectorTolerance/2 && math.Abs(residual[2]) < 1e-12 {
state := context.stateAt(coordinates[0]*rad, coordinates[1]*rad, 0)
return SolarEclipsePathPoint{JDE: jd, Longitude: normalizeLongitude(coordinates[0]), Latitude: coordinates[1], SunAltitude: state.sunAltitudeRad / rad}, true
return SolarEclipsePathPoint{JDE: jde, Longitude: normalizeLongitude(coordinates[0]), Latitude: coordinates[1], SunAltitude: state.sunAltitudeRad / rad}, true
}
var jacobian [3][3]float64
for column := range coordinates {
shifted, shiftedContext := coordinates, context
shifted[column] += steps[column]
if column == 2 {
shiftedContext = solver.localStateContextAt(jd + steps[column]/solarEclipseNonCentralBandTimeScale)
shiftedContext = solver.localStateContextAt(jde + steps[column]/solarEclipseNonCentralBandTimeScale)
}
value := solarCentralBandSkyOffset(shiftedContext, shifted[0], shifted[1])
for row := range residual {
@@ -301,16 +301,16 @@ func (solver solarEclipseSolver) centralBandVectorHorizonRoots(axisContactJDE, d
if !valid {
break
}
jd := axisContactJDE + coordinates[2]/solarEclipseNonCentralBandTimeScale
context := solver.localStateContextAt(jd)
jde := axisContactJDE + coordinates[2]/solarEclipseNonCentralBandTimeScale
context := solver.localStateContextAt(jde)
state := context.stateAt(coordinates[0]*rad, coordinates[1]*rad, 0)
if math.Hypot(residual[0], residual[1]) <= solarEclipseCentralVectorTolerance && math.Abs(state.sunAltitudeRad) < 1e-8 {
roots[i] = SolarEclipsePathPoint{JDE: jd, Longitude: normalizeLongitude(coordinates[0]), Latitude: coordinates[1], SunAltitude: state.sunAltitudeRad / rad}
roots[i] = SolarEclipsePathPoint{JDE: jde, Longitude: normalizeLongitude(coordinates[0]), Latitude: coordinates[1], SunAltitude: state.sunAltitudeRad / rad}
// Grazing horizon roots can be many minutes from axis contact.
// Bound them by the shadow's limb-crossing interval, not a fixed
// window around the seed.
found = jd >= startJDE-solarEclipseCentralLimitHorizonContactMarginDays &&
jd <= endJDE+solarEclipseCentralLimitHorizonContactMarginDays && math.Abs(coordinates[1]) <= 90
found = jde >= startJDE-solarEclipseCentralLimitHorizonContactMarginDays &&
jde <= endJDE+solarEclipseCentralLimitHorizonContactMarginDays && math.Abs(coordinates[1]) <= 90
break
}
matrix := [3][3]float64{jacobian[0], jacobian[1], {}}
@@ -318,7 +318,7 @@ func (solver solarEclipseSolver) centralBandVectorHorizonRoots(axisContactJDE, d
shifted, shiftedContext := coordinates, context
shifted[column] += steps[column]
if column == 2 {
shiftedContext = solver.localStateContextAt(jd + steps[column]/solarEclipseNonCentralBandTimeScale)
shiftedContext = solver.localStateContextAt(jde + steps[column]/solarEclipseNonCentralBandTimeScale)
}
value := shiftedContext.stateAt(shifted[0]*rad, shifted[1]*rad, 0)
matrix[2][column] = (value.sunAltitudeRad - state.sunAltitudeRad) / steps[column]