feat: 完善日月食与月掩几何链路并扩展历法接口

- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
2026-09-17 12:27:40 +08:00
parent 9ee2163cc7
commit 2bf8478639
428 changed files with 85981 additions and 7998 deletions
+49 -16
View File
@@ -9,6 +9,7 @@ import (
"b612.me/astro/basic"
eclipsecore "b612.me/astro/eclipse"
"b612.me/astro/internal/svgchart"
)
const (
@@ -189,8 +190,12 @@ func renderLocalSolarEclipseSVG(
b.WriteString(`<rect width="100%" height="100%" fill="#efefed"/>`)
fmt.Fprintf(&b, `<rect x="22" y="18" width="%.3f" height="%.3f" fill="#ffffff" stroke="#c9c9c6" stroke-width="1.2"/>`,
width-44, height-36)
titleText := title
if options.Title != "" {
titleText = svgchart.EllipsizeText(title, width-52, 26)
}
fmt.Fprintf(&b, `<text x="%.3f" y="44" fill="#111111" font-family="Georgia, 'Times New Roman', serif" font-size="26" font-weight="700" text-anchor="middle">%s</text>`,
width/2, html.EscapeString(title))
width/2, html.EscapeString(titleText))
fmt.Fprintf(&b, `<line x1="%.3f" y1="57" x2="%.3f" y2="57" stroke="#555" stroke-width="1"/>`, width/2-78, width/2+78)
for index, line := range headerTexts {
fontSize := 13
@@ -245,12 +250,23 @@ func renderLocalSolarEclipseSVG(
}
writeLocalSolarEclipseStagePanels(&b, info, eventFrames, options, margin, stageTop, width-2*margin, stageHeight)
direction := localSolarEclipseSVGDirectionTextValue(options)
if options.DirectionText != "" {
direction = svgchart.EllipsizeText(direction, width-80, 12)
}
fmt.Fprintf(&b, `<text x="%.3f" y="%.3f" fill="#333" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>`,
40.0, height-54,
html.EscapeString(localSolarEclipseSVGDirectionTextValue(options)))
note := localSolarEclipseSVGFooterNoteText(options)
fmt.Fprintf(&b, `<text x="%.3f" y="%.3f" fill="#555" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>`,
40.0, height-34, html.EscapeString(note))
40.0, height-54, html.EscapeString(direction))
// 默认说明是单行;调用方文本折行后按画布底边截断,首行位置不变。
lines := []string{localSolarEclipseSVGFooterNoteText(options)}
if options.FooterNote != "" {
maxWidth := width - 80
lines = svgchart.TruncateTextLines(svgchart.WrapText(options.FooterNote, maxWidth, 12), maxWidth, 12,
svgchart.BaselineLineLimit(12, 15, height-34, height-4))
}
for index, line := range lines {
fmt.Fprintf(&b, `<text x="%.3f" y="%.3f" fill="#555" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>`,
40.0, height-34+float64(index)*15, html.EscapeString(line))
}
writeLocalSolarEclipseContacts(&b, info, options, panelX, math.Max(154, cy-92))
b.WriteString(`</svg>`)
return b.String()
@@ -264,16 +280,25 @@ func localSolarEclipseSVGTitleText(info LocalSolarEclipseInfo, options LocalSola
}
func localSolarEclipseSVGHeaderTexts(info LocalSolarEclipseInfo, options LocalSolarEclipseSVGOptions) []string {
lines := []string{
localSolarEclipseSVGSummaryText(info, options),
localSolarEclipseSVGGreatestTextValue(info, options),
localSolarEclipseSVGMetaTextValue(info, options),
values := []struct {
text string
custom bool
}{
{localSolarEclipseSVGSummaryText(info, options), options.SummaryText != ""},
{localSolarEclipseSVGGreatestTextValue(info, options), options.GreatestText != ""},
{localSolarEclipseSVGMetaTextValue(info, options), options.MetaText != ""},
}
filtered := make([]string, 0, len(lines))
for _, line := range lines {
if line != "" {
filtered = append(filtered, line)
filtered := make([]string, 0, len(values))
for _, value := range values {
if value.text == "" {
continue
}
// 摘要行各占一个固定槽位,调用方长文本只能截断,不能折行挤压下一行。
if value.custom {
filtered = append(filtered, svgchart.EllipsizeText(value.text, float64(options.Width)-80, 14))
continue
}
filtered = append(filtered, value.text)
}
return filtered
}
@@ -374,9 +399,17 @@ func localSolarEclipseSVGMetaText(info LocalSolarEclipseInfo, language string) s
parts := make([]string, 0, 2)
if info.HasSaros {
if language == localSolarEclipseSVGLanguageEnglish {
parts = append(parts, fmt.Sprintf("Solar Saros %d %d/%d", info.Saros.Series, info.Saros.Member, info.Saros.Count))
label := fmt.Sprintf("Solar Saros %d %d/%d", info.Saros.Series, info.Saros.Member, info.Saros.Count)
if !info.Saros.Verified {
label += " (provisional)"
}
parts = append(parts, label)
} else {
parts = append(parts, fmt.Sprintf("沙罗 %d 第 %d/%d 个成员", info.Saros.Series, info.Saros.Member, info.Saros.Count))
label := fmt.Sprintf("沙罗 %d 第 %d/%d 个成员", info.Saros.Series, info.Saros.Member, info.Saros.Count)
if !info.Saros.Verified {
label += "(推算)"
}
parts = append(parts, label)
}
}
if duration := localSolarEclipseSVGCentralDurationText(info, language); duration != "" {