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
+48 -22
View File
@@ -9,6 +9,7 @@ import (
"time"
"b612.me/astro/internal/svgasset"
"b612.me/astro/internal/svgchart"
"b612.me/astro/moon"
)
@@ -57,11 +58,12 @@ type LocalPlanetOccultationSVGOptions struct {
}
type localPlanetOccultationEventFrame struct {
label string
name string
time time.Time
hidden bool
frame moon.PlanetOccultationDiagramFrame
label string
name string
time time.Time
hidden bool
frame moon.PlanetOccultationDiagramFrame
appearance localOccultationMoonAppearance
}
// FindLocalPlanetOccultationSVGs 搜索固定观测地点,并渲染请求时间窗口内的每次有限盘面行星月掩。空切片表示没有本地事件。
@@ -192,6 +194,9 @@ func renderLocalPlanetOccultationSVG(
) string {
title := localPlanetOccultationSVGTitle(info, options)
headerLines := localPlanetOccultationSVGHeaderLines(info, options)
// 页眉字号 14/13,省略号按较大的 14 估算;行数上限只与画布高度有关,先按空页眉取上限。
headerLimit := localPlanetOccultationSVGLayoutFor(options, 72).headerMaxLines
headerLines = svgchart.TruncateTextLines(headerLines, float64(options.Width)-80, 14, headerLimit)
headerBottom := 72.0 + float64(len(headerLines))*19
layout := localPlanetOccultationSVGLayoutFor(options, headerBottom)
events := localPlanetOccultationSVGEventFrames(info, diagram.Frames, options.Language)
@@ -205,8 +210,13 @@ func renderLocalPlanetOccultationSVG(
b.WriteString(`</defs>`)
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"/>`, layout.width-44, layout.height-36)
titleSize := starOccultationTitleFontSize(title, layout.width)
titleText := title
if options.Title != "" {
titleText = svgchart.EllipsizeText(title, layout.width-80, float64(titleSize))
}
fmt.Fprintf(&b, `<text x="%.3f" y="44" fill="#111111" font-family="Georgia, 'Times New Roman', serif" font-size="%d" font-weight="700" text-anchor="middle">%s</text>`,
layout.width/2, starOccultationTitleFontSize(title, layout.width), html.EscapeString(title))
layout.width/2, titleSize, html.EscapeString(titleText))
fmt.Fprintf(&b, `<line x1="%.3f" y1="57" x2="%.3f" y2="57" stroke="#555" stroke-width="1"/>`, layout.width/2-78, layout.width/2+78)
for index, line := range headerLines {
fontSize, fill := 13, "#3b4143"
@@ -255,6 +265,7 @@ func localPlanetOccultationSVGLayoutFor(
stageTop: stageTop,
stageBottom: stageBottom,
footerY: height - 62,
headerMaxLines: localOccultationHeaderLineLimit(height, footerSpace, stageHeight),
}
}
@@ -265,8 +276,10 @@ func writeLocalPlanetOccultationOverview(
layout localStarOccultationSVGLayout,
options LocalPlanetOccultationSVGOptions,
) {
overviewTitle := occultationTitleText(localPlanetOccultationSVGOverviewTitle(options), options.OverviewTitle != "",
layout.panelX-layout.overviewLeft-12, 14)
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#161a1b" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700">%s</text>`,
layout.overviewLeft, layout.overviewTop-10, html.EscapeString(localPlanetOccultationSVGOverviewTitle(options)))
layout.overviewLeft, layout.overviewTop-10, html.EscapeString(overviewTitle))
cx := (layout.overviewLeft + layout.overviewRight) / 2
cy := (layout.overviewTop+layout.overviewBottom)/2 + 4
extent := localPlanetOccultationSVGExtent(diagram.Frames)
@@ -289,7 +302,9 @@ func writeLocalPlanetOccultationOverview(
event.frame.PlanetRadiusArcsec*scale, false, "overview-planet-disk", event.label,
)
}
writeLocalStarOccultationMoon(b, cx, cy, moonRadius, "overview-moon")
writeLocalOccultationMoon(b, cx, cy, moonRadius, "overview-moon",
localOccultationMoonAppearanceAt(diagram.Occultation.Greatest, diagram.Occultation.Observer),
"local-planet-occultation-moon-overview")
for _, event := range events {
x := mapX(event.frame.PlanetXArcsec)
y := mapY(event.frame.PlanetYArcsec)
@@ -435,8 +450,10 @@ func writeLocalPlanetOccultationContacts(
) {
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#d0d3d1" stroke-width="1"/>`,
layout.panelX-12, layout.overviewTop-12, layout.panelX-12, layout.overviewBottom)
contactsTitle := occultationTitleText(localPlanetOccultationSVGContactsTitle(options), options.ContactsTitle != "",
layout.width-layout.panelX-layout.margin, 14)
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#161a1b" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700">%s</text>`,
layout.panelX, layout.overviewTop-10, html.EscapeString(localPlanetOccultationSVGContactsTitle(options)))
layout.panelX, layout.overviewTop-10, html.EscapeString(contactsTitle))
available := layout.overviewBottom - layout.overviewTop - 10
rowHeight := math.Min(66, available/math.Max(1, float64(len(events))))
for index, event := range events {
@@ -465,8 +482,10 @@ func writeLocalPlanetOccultationStages(
layout localStarOccultationSVGLayout,
options LocalPlanetOccultationSVGOptions,
) {
phaseTitle := occultationTitleText(localPlanetOccultationSVGPhasePanelsTitle(options), options.PhasePanelsTitle != "",
layout.width-2*layout.margin, 14)
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#161a1b" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700">%s</text>`,
layout.margin, layout.stageTop+13, html.EscapeString(localPlanetOccultationSVGPhasePanelsTitle(options)))
layout.margin, layout.stageTop+13, html.EscapeString(phaseTitle))
if len(events) == 0 {
return
}
@@ -484,7 +503,8 @@ func writeLocalPlanetOccultationStages(
x, y := localPlanetOccultationSVGStageCenter(event, cx, cy, radius, displayRadius, scale)
fmt.Fprintf(b, `<circle class="stage-planet-disk" data-label="%s" data-true-radius="%.3f" data-magnification="%.3f" cx="%.3f" cy="%.3f" r="%.3f" fill="#d6ad69" stroke="#704b2d" stroke-width="1.1"/>`,
html.EscapeString(event.label), trueRadius, magnification, x, y, displayRadius)
writeLocalStarOccultationMoon(b, cx, cy, event.frame.MoonRadiusArcsec*scale, "stage-moon")
writeLocalOccultationMoon(b, cx, cy, event.frame.MoonRadiusArcsec*scale, "stage-moon",
event.appearance, fmt.Sprintf("local-planet-occultation-moon-stage-%d", index))
if event.hidden {
fmt.Fprintf(b, `<circle class="stage-planet-hidden-outline" cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#7d3430" stroke-width="1.1" stroke-dasharray="2 2"/>`,
x, y, displayRadius)
@@ -527,12 +547,14 @@ func writeLocalPlanetOccultationFooter(
layout localStarOccultationSVGLayout,
options LocalPlanetOccultationSVGOptions,
) {
directionLines := starOccultationWrapText(localPlanetOccultationSVGDirectionText(options), layout.width-80, 10)
directionLines, footerLines := localOccultationFooterLines(
localPlanetOccultationSVGDirectionText(options), options.DirectionText != "",
localPlanetOccultationSVGFooterNote(info, options), options.FooterNote != "",
layout.footerY, layout.width, layout.height)
for index, line := range directionLines {
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" font-family="Georgia, 'Times New Roman', serif" font-size="10">%s</text>`,
layout.margin, layout.footerY+float64(index)*14-8, html.EscapeString(line))
}
footerLines := starOccultationWrapText(localPlanetOccultationSVGFooterNote(info, options), layout.width-80, 9)
for index, line := range footerLines {
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#747c7d" font-family="Georgia, 'Times New Roman', serif" font-size="9">%s</text>`,
layout.margin, layout.footerY+float64(len(directionLines))*14+float64(index)*12-6, html.EscapeString(line))
@@ -562,11 +584,12 @@ func localPlanetOccultationSVGEventFrames(
hidden := frame.FullyOcculted || info.Type == moon.OccultationTotal &&
(label == "C2" || label == "Greatest" || label == "C3")
result = append(result, localPlanetOccultationEventFrame{
label: label,
name: localPlanetOccultationSVGEventName(label, language),
time: times[label],
hidden: hidden,
frame: frame,
label: label,
name: localPlanetOccultationSVGEventName(label, language),
time: times[label],
hidden: hidden,
frame: frame,
appearance: localOccultationMoonAppearanceAt(times[label], info.Observer),
})
break
}
@@ -592,11 +615,14 @@ func localPlanetOccultationSVGHeaderLines(
options LocalPlanetOccultationSVGOptions,
) []string {
lines := make([]string, 0, 4)
for _, value := range []string{
localPlanetOccultationSVGSummaryText(info, options),
localPlanetOccultationSVGGreatestText(info, options),
for _, item := range []struct {
value string
custom bool
}{
{localPlanetOccultationSVGSummaryText(info, options), options.SummaryText != ""},
{localPlanetOccultationSVGGreatestText(info, options), options.GreatestText != ""},
} {
lines = append(lines, starOccultationWrapText(value, float64(options.Width)-80, 13)...)
lines = append(lines, occultationWrappedTextLines(item.value, item.custom, float64(options.Width)-80, 13)...)
}
return lines
}