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
+81 -28
View File
@@ -9,6 +9,7 @@ import (
"time"
"b612.me/astro/internal/svgasset"
"b612.me/astro/internal/svgchart"
"b612.me/astro/moon"
)
@@ -69,13 +70,15 @@ type localStarOccultationSVGLayout struct {
stageTop float64
stageBottom float64
footerY float64
headerMaxLines int
}
type localStarOccultationEventFrame struct {
label string
name string
time time.Time
frame moon.StarOccultationDiagramFrame
label string
name string
time time.Time
frame moon.StarOccultationDiagramFrame
appearance localOccultationMoonAppearance
}
// FindLocalStarOccultationSVGs 搜索固定观测地点,并渲染请求时间窗口内的每次点光源恒星月掩。不会加载内嵌星表;空切片表示没有本地事件。
@@ -198,6 +201,9 @@ func renderLocalStarOccultationSVG(
) string {
title := localStarOccultationSVGTitle(info, options)
headerLines := localStarOccultationSVGHeaderLines(info, options)
// 页眉字号 14/13,省略号按较大的 14 估算;行数上限只与画布高度有关,先按空页眉取上限。
headerLimit := localStarOccultationSVGLayoutFor(options, 72).headerMaxLines
headerLines = svgchart.TruncateTextLines(headerLines, float64(options.Width)-80, 14, headerLimit)
headerBottom := 72.0 + float64(len(headerLines))*19
layout := localStarOccultationSVGLayoutFor(options, headerBottom)
events := localStarOccultationSVGEventFrames(info, diagram.Frames, options.Language)
@@ -211,8 +217,13 @@ func renderLocalStarOccultationSVG(
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"
@@ -258,6 +269,7 @@ func localStarOccultationSVGLayoutFor(options LocalStarOccultationSVGOptions, he
stageTop: stageTop,
stageBottom: stageBottom,
footerY: height - 43,
headerMaxLines: localOccultationHeaderLineLimit(height, footerSpace, stageHeight),
}
}
@@ -268,8 +280,10 @@ func writeLocalStarOccultationOverview(
layout localStarOccultationSVGLayout,
options LocalStarOccultationSVGOptions,
) {
overviewTitle := occultationTitleText(localStarOccultationSVGOverviewTitle(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(localStarOccultationSVGOverviewTitle(options)))
layout.overviewLeft, layout.overviewTop-10, html.EscapeString(overviewTitle))
cx := (layout.overviewLeft + layout.overviewRight) / 2
cy := (layout.overviewTop+layout.overviewBottom)/2 + 4
extent := localStarOccultationSVGExtent(diagram.Frames)
@@ -284,7 +298,9 @@ func writeLocalStarOccultationOverview(
moonRadius := localStarOccultationSVGMaximumMoonRadius(diagram.Frames) * scale
writeLocalStarOccultationAxes(b, cx, cy, moonRadius, options.Language)
writeLocalStarOccultationMoon(b, cx, cy, moonRadius, "overview-moon")
writeLocalOccultationMoon(b, cx, cy, moonRadius, "overview-moon",
localOccultationMoonAppearanceAt(diagram.Occultation.Greatest, diagram.Occultation.Observer),
"local-star-occultation-moon-overview")
writeLocalStarOccultationLunarPath(b, diagram.Frames, mapX, mapY, extent, options.Language)
writeLocalStarOccultationTrack(b, diagram.Frames, mapX, mapY)
for _, event := range events {
@@ -400,14 +416,6 @@ func localStarOccultationSVGLunarPathLabel(language string) string {
return "白道"
}
func writeLocalStarOccultationMoon(b *strings.Builder, cx, cy, radius float64, class string) {
fmt.Fprintf(b, `<g class="%s">`, html.EscapeString(class))
fmt.Fprintf(b, `<use href="#le-moon" x="%.3f" y="%.3f" width="%.3f" height="%.3f"/>`,
cx-radius, cy-radius, radius*2, radius*2)
fmt.Fprintf(b, `<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#4f5d60" stroke-width="1.1" opacity="0.9"/>`, cx, cy, radius)
b.WriteString(`</g>`)
}
func writeLocalStarOccultationAxes(b *strings.Builder, cx, cy, radius float64, language string) {
north, east, west, south := "北", "东", "西", "南"
if language == starOccultationSVGLanguageEnglish {
@@ -466,8 +474,10 @@ func writeLocalStarOccultationContacts(
) {
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(localStarOccultationSVGContactsTitle(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(localStarOccultationSVGContactsTitle(options)))
layout.panelX, layout.overviewTop-10, html.EscapeString(contactsTitle))
available := layout.overviewBottom - layout.overviewTop - 10
rowHeight := math.Min(86, available/math.Max(1, float64(len(events))))
for index, event := range events {
@@ -494,8 +504,10 @@ func writeLocalStarOccultationStages(
layout localStarOccultationSVGLayout,
options LocalStarOccultationSVGOptions,
) {
phaseTitle := occultationTitleText(localStarOccultationSVGPhasePanelsTitle(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(localStarOccultationSVGPhasePanelsTitle(options)))
layout.margin, layout.stageTop+13, html.EscapeString(phaseTitle))
if len(events) == 0 {
return
}
@@ -507,7 +519,8 @@ func writeLocalStarOccultationStages(
cy := layout.stageTop + 34 + radius
for index, event := range events {
cx := layout.margin + columnWidth*(float64(index)+0.5)
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-star-occultation-moon-stage-%d", index))
x := cx - event.frame.StarXArcsec*scale
y := cy - event.frame.StarYArcsec*scale
writeLocalStarOccultationStar(b, x, y, event.frame.BehindMoon, "stage-star", event.label)
@@ -531,12 +544,14 @@ func writeLocalStarOccultationFooter(
layout localStarOccultationSVGLayout,
options LocalStarOccultationSVGOptions,
) {
directionLines := starOccultationWrapText(localStarOccultationSVGDirectionText(options), layout.width-80, 10)
directionLines, footerLines := localOccultationFooterLines(
localStarOccultationSVGDirectionText(options), options.DirectionText != "",
localStarOccultationSVGFooterNote(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(localStarOccultationSVGFooterNote(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))
@@ -559,10 +574,11 @@ func localStarOccultationSVGEventFrames(
for _, frame := range frames {
if localStarOccultationSVGFrameHasLabel(frame, label) {
result = append(result, localStarOccultationEventFrame{
label: label,
name: localStarOccultationSVGEventName(label, language),
time: times[label],
frame: frame,
label: label,
name: localStarOccultationSVGEventName(label, language),
time: times[label],
frame: frame,
appearance: localOccultationMoonAppearanceAt(times[label], info.Observer),
})
break
}
@@ -585,15 +601,52 @@ func localStarOccultationSVGFrameHasLabel(frame moon.StarOccultationDiagramFrame
func localStarOccultationSVGHeaderLines(info moon.StarOccultationInfo, options LocalStarOccultationSVGOptions) []string {
lines := make([]string, 0, 4)
for _, value := range []string{
localStarOccultationSVGSummaryText(info, options),
localStarOccultationSVGGreatestText(info, options),
for _, item := range []struct {
value string
custom bool
}{
{localStarOccultationSVGSummaryText(info, options), options.SummaryText != ""},
{localStarOccultationSVGGreatestText(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
}
// localOccultationFooterLines 排布本地图页脚:补充说明贴画布底边,方向说明占用上方余高,两块都按行数上限截断。
func localOccultationFooterLines(
directionText string, directionCustom bool,
noteText string, noteCustom bool,
footerY, width, height float64,
) ([]string, []string) {
maxWidth := width - 80
direction := occultationWrappedTextLines(directionText, directionCustom, maxWidth, 10)
note := occultationWrappedTextLines(noteText, noteCustom, maxWidth, 9)
noteAbove, noteBelow := svgchart.EstimatedTextExtents(9)
noteHeight := noteAbove + float64(len(note)-1)*12 + noteBelow
direction = svgchart.TruncateTextLines(direction, maxWidth, 10,
svgchart.BaselineLineLimit(10, 14, footerY-8, height-2-noteHeight))
noteFirst := footerY + float64(len(direction))*14 - 6
note = svgchart.TruncateTextLines(note, maxWidth, 9,
svgchart.BaselineLineLimit(9, 12, noteFirst, height-2))
return direction, note
}
// localOccultationHeaderLineLimit 由版式的页脚预留与阶段区高度推出页眉行数上限,总览区被压到 120 px 以下时圆盘与方向标记会跑出画布。
func localOccultationHeaderLineLimit(height, footerSpace, stageHeight float64) int {
const (
stageGap = 20.0
overviewTopGap = 26.0
overviewMin = 120.0
headerStart = 72.0
)
limit := int(math.Floor((height - footerSpace - stageHeight - stageGap - overviewMin - overviewTopGap - headerStart) / 19))
if limit < 1 {
return 1
}
return limit
}
func localStarOccultationSVGTitle(info moon.StarOccultationInfo, options LocalStarOccultationSVGOptions) string {
if options.Title != "" {
return options.Title