astro/eclipse/svg/solar_model.go
starainrt 3ffdbe0034
feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
2026-05-01 22:38:44 +08:00

157 lines
4.8 KiB
Go

package svg
import (
"math"
"time"
"b612.me/astro/basic"
eclipsecore "b612.me/astro/eclipse"
)
type SolarEclipseRadiusModel = eclipsecore.SolarEclipseRadiusModel
type SolarEclipseType = eclipsecore.SolarEclipseType
type LocalSolarEclipseContactPoint = eclipsecore.LocalSolarEclipseContactPoint
type LocalSolarEclipseInfo = eclipsecore.LocalSolarEclipseInfo
const (
SolarEclipseModelIAUSingleK = eclipsecore.SolarEclipseModelIAUSingleK
SolarEclipseModelNASABulletinSplitK = eclipsecore.SolarEclipseModelNASABulletinSplitK
SolarEclipseNone = eclipsecore.SolarEclipseNone
SolarEclipsePartial = eclipsecore.SolarEclipsePartial
SolarEclipseAnnular = eclipsecore.SolarEclipseAnnular
SolarEclipseTotal = eclipsecore.SolarEclipseTotal
SolarEclipseHybrid = eclipsecore.SolarEclipseHybrid
)
func localSolarEclipseInfoFromDiagram(
diagram basic.LocalSolarEclipseDiagramResult,
lon, lat, height float64,
location *time.Location,
) LocalSolarEclipseInfo {
info := localSolarEclipseInfoFieldsFromBasic(diagram.Eclipse, lon, lat, height, location)
info.ContactPoints = localSolarEclipseContactPointsFromFrames(diagram.Frames, location)
return info
}
func localSolarEclipseInfoFieldsFromBasic(
result basic.LocalSolarEclipseResult,
lon, lat, height float64,
location *time.Location,
) LocalSolarEclipseInfo {
visibleThreshold := localSolarEclipseVisibilityThreshold(height, lat)
return LocalSolarEclipseInfo{
Model: mapBasicSolarEclipseModel(result.Model),
Type: mapBasicSolarEclipseType(result.Type),
Longitude: lon,
Latitude: lat,
Height: height,
GreatestEclipse: solarEclipseTTJDEToTime(result.GreatestEclipse, location),
PartialStart: solarEclipseTTJDEToTime(result.PartialStart, location),
PartialEnd: solarEclipseTTJDEToTime(result.PartialEnd, location),
CentralStart: solarEclipseTTJDEToTime(result.CentralStart, location),
CentralEnd: solarEclipseTTJDEToTime(result.CentralEnd, location),
Magnitude: result.Magnitude,
Obscuration: result.Obscuration,
Separation: result.Separation,
SunAltitude: result.SunAltitude,
SunAzimuth: result.SunAzimuth,
VisibleAtGreatest: result.SunAltitude > visibleThreshold,
HasPartial: result.HasPartial,
HasCentral: result.HasCentral,
HasAnnular: result.HasAnnular,
HasTotal: result.HasTotal,
}
}
func localSolarEclipseContactPointsFromFrames(
frames []basic.LocalSolarEclipseDiagramFrame,
location *time.Location,
) []LocalSolarEclipseContactPoint {
contacts := make([]LocalSolarEclipseContactPoint, 0, 4)
for _, frame := range frames {
for _, label := range localSolarEclipseFrameLabels(frame) {
switch label {
case "C1", "C2", "C3", "C4":
contactPA := frame.PositionAngle
if (label == "C2" || label == "C3") && frame.MoonRadius >= frame.SunRadius {
contactPA = normalizeSolarEclipseDegree360(contactPA + 180)
}
contacts = append(contacts, LocalSolarEclipseContactPoint{
Label: label,
Time: solarEclipseTTJDEToTime(frame.JDE, location),
ContactPositionAngle: contactPA,
ContactClockwiseAngle: normalizeSolarEclipseDegree360(360 - contactPA),
MoonCenterPositionAngle: frame.PositionAngle,
})
}
}
}
return contacts
}
func localSolarEclipseFrameLabels(frame basic.LocalSolarEclipseDiagramFrame) []string {
if len(frame.Labels) > 0 {
return frame.Labels
}
if frame.Label == "" {
return nil
}
return []string{frame.Label}
}
func mapBasicSolarEclipseModel(model basic.SolarEclipseRadiusModel) SolarEclipseRadiusModel {
switch model {
case basic.SolarEclipseModelIAUSingleK:
return SolarEclipseModelIAUSingleK
default:
return SolarEclipseModelNASABulletinSplitK
}
}
func mapBasicSolarEclipseType(eclipseType basic.SolarEclipseType) SolarEclipseType {
switch eclipseType {
case basic.SolarEclipsePartial:
return SolarEclipsePartial
case basic.SolarEclipseAnnular:
return SolarEclipseAnnular
case basic.SolarEclipseTotal:
return SolarEclipseTotal
case basic.SolarEclipseHybrid:
return SolarEclipseHybrid
default:
return SolarEclipseNone
}
}
func solarEclipseTTJDEToTime(ttJDE float64, location *time.Location) time.Time {
if ttJDE == 0 {
return time.Time{}
}
utcJDE := basic.TD2UT(ttJDE, false)
return basic.JDE2DateByZone(utcJDE, location, false)
}
func solarEclipseTimeToTTJDE(date time.Time) float64 {
utcJDE := basic.Date2JDE(date.UTC())
return basic.TD2UT(utcJDE, true)
}
func localSolarEclipseVisibilityThreshold(height, latitude float64) float64 {
if height <= 0 {
return 0
}
return -basic.HeightDegreeByLat(height, latitude)
}
func normalizeSolarEclipseDegree360(angle float64) float64 {
angle = math.Mod(angle, 360)
if angle < 0 {
angle += 360
}
return angle
}