feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// Package svgchart 提供日月食与月掩星详细版式共用的数据块与版面计算。
|
||||
// Package svgchart holds the data blocks and page geometry shared by the detailed eclipse and occultation charts.
|
||||
package svgchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PanelRow 是数据块里的一行;Detail 非空时在数值下方补一行灰色小字。
|
||||
type PanelRow struct {
|
||||
Label string
|
||||
Value string
|
||||
Detail string
|
||||
}
|
||||
|
||||
// PanelBlock 是一个带标题的数据块。
|
||||
type PanelBlock struct {
|
||||
Title string
|
||||
Rows []PanelRow
|
||||
}
|
||||
|
||||
// WritePanelBox 画一个数据块:标题加若干行“标签 + 数值”。
|
||||
// 行高按行数等分,因此块高变化时不会溢出;Class 用于区分各族图的样式钩子。
|
||||
func WritePanelBox(
|
||||
b *strings.Builder,
|
||||
class string,
|
||||
x, y, width, height float64,
|
||||
title string,
|
||||
rows []PanelRow,
|
||||
) {
|
||||
if len(rows) == 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(b, `<g class="%s">`, class)
|
||||
fmt.Fprintf(b, `<rect x="%.3f" y="%.3f" width="%.3f" height="%.3f" rx="3" fill="#fbfcfd" stroke="#dde3e9"/>`,
|
||||
x, y, width, height)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111827" font-family="Georgia, 'Times New Roman', serif" font-size="12" font-weight="700">%s</text>`,
|
||||
x+12, y+20, html.EscapeString(title))
|
||||
rowHeight := (height - panelBoxHeaderHeight) / float64(len(rows))
|
||||
for index, row := range rows {
|
||||
if row.Label == "" {
|
||||
continue
|
||||
}
|
||||
rowY := y + panelBoxHeaderHeight + float64(index)*rowHeight
|
||||
size := PanelRowFontSize(width, row)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#475569" font-family="Arial, sans-serif" font-size="%.1f">%s</text>`,
|
||||
x+panelBoxPadding, rowY+11, size, html.EscapeString(row.Label))
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#0f172a" font-family="Arial, sans-serif" font-size="%.1f" font-weight="700" text-anchor="end">%s</text>`,
|
||||
x+width-panelBoxPadding, rowY+11, size, html.EscapeString(row.Value))
|
||||
if row.Detail != "" {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#7b8794" font-family="Arial, sans-serif" font-size="%d">%s</text>`,
|
||||
x+panelBoxPadding, rowY+22, panelRowDetailSize, html.EscapeString(row.Detail))
|
||||
}
|
||||
}
|
||||
b.WriteString(`</g>`)
|
||||
}
|
||||
|
||||
const (
|
||||
panelBoxHeaderHeight = 30
|
||||
panelRowLabelSize = 10.5
|
||||
panelRowDetailSize = 9
|
||||
panelBoxPadding = 12
|
||||
panelLabelValueGap = 8
|
||||
panelMinimumFontSize = 8
|
||||
)
|
||||
|
||||
// EstimatedTextWidth 按字号估算文本宽度:ASCII 取 0.62em,非 ASCII(含全角中文)取 1.05em。
|
||||
// 没有字体度量可用,这里只服务于压叠判定,估值偏大以保证判定保守。
|
||||
// EstimatedTextWidth estimates text width from the font size: 0.62em per ASCII rune and 1.05em
|
||||
// otherwise. It exists only for overlap checks, so it deliberately over-estimates.
|
||||
func EstimatedTextWidth(text string, fontSize float64) float64 {
|
||||
units := 0.0
|
||||
for _, symbol := range text {
|
||||
if symbol < 128 {
|
||||
units += 0.62
|
||||
continue
|
||||
}
|
||||
units += 1.05
|
||||
}
|
||||
return units * fontSize
|
||||
}
|
||||
|
||||
// EstimatedTextExtents 返回文本相对基线的上下留白,用于构造占位矩形。
|
||||
// EstimatedTextExtents returns the space a text keeps above and below its baseline.
|
||||
func EstimatedTextExtents(fontSize float64) (above, below float64) {
|
||||
return 0.85 * fontSize, 0.30 * fontSize
|
||||
}
|
||||
|
||||
// PanelRowFontSize 返回该行“标签 + 数值”放不下时等比缩小后的字号;缩小到下限仍放不下说明块太窄。
|
||||
// PanelRowFontSize shrinks a row's font until its label and value fit side by side.
|
||||
func PanelRowFontSize(width float64, row PanelRow) float64 {
|
||||
content := width - 2*panelBoxPadding
|
||||
if content <= 0 {
|
||||
return panelMinimumFontSize
|
||||
}
|
||||
pair := EstimatedTextWidth(row.Label, panelRowLabelSize) + EstimatedTextWidth(row.Value, panelRowLabelSize)
|
||||
if pair+panelLabelValueGap <= content {
|
||||
return panelRowLabelSize
|
||||
}
|
||||
size := panelRowLabelSize * (content - panelLabelValueGap) / pair
|
||||
if size < panelMinimumFontSize {
|
||||
return panelMinimumFontSize
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func panelTextRect(x, baseline, fontSize float64, text string, anchorEnd bool) panelTextBox {
|
||||
width := EstimatedTextWidth(text, fontSize)
|
||||
left := x
|
||||
if anchorEnd {
|
||||
left = x - width
|
||||
}
|
||||
above, below := EstimatedTextExtents(fontSize)
|
||||
return panelTextBox{x: left, y: baseline - above, width: width, height: above + below}
|
||||
}
|
||||
|
||||
// panelTextBox 是文本的保守包围盒。
|
||||
type panelTextBox struct {
|
||||
x, y, width, height float64
|
||||
}
|
||||
|
||||
func panelTextBoxesOverlap(first, second panelTextBox) bool {
|
||||
left, right := math.Max(first.x, second.x), math.Min(first.x+first.width, second.x+second.width)
|
||||
top, bottom := math.Max(first.y, second.y), math.Min(first.y+first.height, second.y+second.height)
|
||||
return right-left > 0.5 && bottom-top > 0
|
||||
}
|
||||
|
||||
// PanelBoxOverlaps 报告该块在给定位次与尺寸下,块内文本是否会互相压叠。
|
||||
// 行高按行数等分,所以块太矮时行距会小于文字高度,此时必须由调用方拒绝该画布而不是画出压叠的文字。
|
||||
// PanelBoxOverlaps reports whether the block's own texts would collide at the given box; callers must
|
||||
// reject a canvas whose panels cannot keep the rows apart.
|
||||
func PanelBoxOverlaps(x, y, width, height float64, title string, rows []PanelRow) bool {
|
||||
if len(rows) == 0 || height <= 0 {
|
||||
return false
|
||||
}
|
||||
boxes := make([]panelTextBox, 0, 2*len(rows)+1)
|
||||
boxes = append(boxes, panelTextRect(x+12, y+20, 12, title, false))
|
||||
rowHeight := (height - panelBoxHeaderHeight) / float64(len(rows))
|
||||
for index, row := range rows {
|
||||
if row.Label == "" {
|
||||
continue
|
||||
}
|
||||
rowY := y + panelBoxHeaderHeight + float64(index)*rowHeight
|
||||
size := PanelRowFontSize(width, row)
|
||||
boxes = append(boxes, panelTextRect(x+panelBoxPadding, rowY+11, size, row.Label, false))
|
||||
boxes = append(boxes, panelTextRect(x+width-panelBoxPadding, rowY+11, size, row.Value, true))
|
||||
if row.Detail != "" {
|
||||
boxes = append(boxes, panelTextRect(x+panelBoxPadding, rowY+22, panelRowDetailSize, row.Detail, false))
|
||||
}
|
||||
}
|
||||
for first := 0; first < len(boxes); first++ {
|
||||
for second := first + 1; second < len(boxes); second++ {
|
||||
if panelTextBoxesOverlap(boxes[first], boxes[second]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user