2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
163 lines
6.0 KiB
Go
163 lines
6.0 KiB
Go
// 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
|
|
}
|