2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
128 lines
4.9 KiB
Go
128 lines
4.9 KiB
Go
package svgchart
|
||
|
||
// LabelBox 是文本或图形占位的保守包围盒。/ LabelBox is a conservative bounding box for a reserved label or graphic.
|
||
type LabelBox struct {
|
||
X, Y, Width, Height float64
|
||
}
|
||
|
||
// LabelPlacement 是一个候选位次:文本锚点与 text-anchor 取值。
|
||
// LabelPlacement is one candidate slot: the text anchor point and its text-anchor value.
|
||
type LabelPlacement struct {
|
||
X, Y float64
|
||
Anchor string
|
||
}
|
||
|
||
// LabelTable 记录已占位的矩形,后来的标签按候选顺序避让。
|
||
// LabelTable records reserved rectangles; later labels avoid them in candidate order.
|
||
type LabelTable struct {
|
||
boxes []LabelBox
|
||
}
|
||
|
||
// labelClearance 是落位判定要求的间隙:包围盒各向外放 1 px,写出的坐标取整后两个盒子也不会贴成相交。
|
||
const labelClearance = 1.0
|
||
|
||
// Reserve 占位一个矩形;非正宽高直接忽略。
|
||
// Reserve claims a rectangle; non-positive extents are ignored.
|
||
func (table *LabelTable) Reserve(x, y, width, height float64) {
|
||
if width <= 0 || height <= 0 {
|
||
return
|
||
}
|
||
table.boxes = append(table.boxes, LabelBox{X: x, Y: y, Width: width, Height: height})
|
||
}
|
||
|
||
// LabelTextBox 返回文本的保守包围盒,anchor 取 start、middle 或 end。
|
||
// LabelTextBox returns a text's conservative box for a start, middle, or end anchor.
|
||
func LabelTextBox(x, baseline, fontSize float64, text, anchor string) LabelBox {
|
||
if fontSize <= 0 {
|
||
fontSize = 12
|
||
}
|
||
width := EstimatedTextWidth(text, fontSize)
|
||
left := x
|
||
switch anchor {
|
||
case "middle":
|
||
left = x - width/2
|
||
case "end":
|
||
left = x - width
|
||
}
|
||
above, below := EstimatedTextExtents(fontSize)
|
||
return LabelBox{X: left, Y: baseline - above, Width: width, Height: above + below}
|
||
}
|
||
|
||
// ReserveText 占位一段文本并返回它的包围盒。
|
||
// ReserveText claims a text box and returns it.
|
||
func (table *LabelTable) ReserveText(x, baseline, fontSize float64, text, anchor string) LabelBox {
|
||
box := LabelTextBox(x, baseline, fontSize, text, anchor)
|
||
table.Reserve(box.X, box.Y, box.Width, box.Height)
|
||
return box
|
||
}
|
||
|
||
// Overlaps 报告矩形是否压到已占位矩形;两个盒子之间至少留 labelClearance。
|
||
// Overlaps reports whether a rectangle collides with any reserved box, keeping labelClearance between them.
|
||
func (table *LabelTable) Overlaps(x, y, width, height float64) bool {
|
||
for _, box := range table.boxes {
|
||
if box.X+box.Width+labelClearance <= x || x+width+labelClearance <= box.X {
|
||
continue
|
||
}
|
||
if box.Y+box.Height+labelClearance <= y || y+height+labelClearance <= box.Y {
|
||
continue
|
||
}
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// OverlapsText 报告文本是否压到已占位矩形。
|
||
// OverlapsText reports whether a text collides with any reserved box.
|
||
func (table *LabelTable) OverlapsText(x, baseline, fontSize float64, text, anchor string) bool {
|
||
box := LabelTextBox(x, baseline, fontSize, text, anchor)
|
||
return table.Overlaps(box.X, box.Y, box.Width, box.Height)
|
||
}
|
||
|
||
// Place 按候选顺序放一段文本:返回首个不压叠的位次并占位;全部压叠时返回 false 且不占位。
|
||
// Place takes the first candidate that keeps the text clear, claims it, and reports false when none does.
|
||
func (table *LabelTable) Place(text string, fontSize float64, candidates []LabelPlacement) (LabelPlacement, bool) {
|
||
for _, candidate := range candidates {
|
||
box := LabelTextBox(candidate.X, candidate.Y, fontSize, text, candidate.Anchor)
|
||
if table.Overlaps(box.X, box.Y, box.Width, box.Height) {
|
||
continue
|
||
}
|
||
table.Reserve(box.X, box.Y, box.Width, box.Height)
|
||
return candidate, true
|
||
}
|
||
return LabelPlacement{}, false
|
||
}
|
||
|
||
// MirrorLabelAnchor 返回镜像后的 text-anchor:start 与 end 互换,middle 不变。
|
||
// MirrorLabelAnchor flips start and end, and keeps middle.
|
||
func MirrorLabelAnchor(anchor string) string {
|
||
switch anchor {
|
||
case "start":
|
||
return "end"
|
||
case "end":
|
||
return "start"
|
||
default:
|
||
return "middle"
|
||
}
|
||
}
|
||
|
||
// LabelCandidates 生成绕锚点的候选位次:原位、上下左右、四角,再按镜像 anchor 重复一遍。
|
||
// LabelCandidates lists candidate slots around an anchor: in place, then the four sides, then the
|
||
// four corners, repeated for the mirrored anchor.
|
||
func LabelCandidates(x, y float64, anchor string, radius float64) []LabelPlacement {
|
||
candidates := make([]LabelPlacement, 0, 18)
|
||
for _, current := range []string{anchor, MirrorLabelAnchor(anchor)} {
|
||
candidates = append(candidates,
|
||
LabelPlacement{X: x, Y: y, Anchor: current},
|
||
LabelPlacement{X: x, Y: y - radius, Anchor: current},
|
||
LabelPlacement{X: x, Y: y + radius, Anchor: current},
|
||
LabelPlacement{X: x + radius, Y: y, Anchor: current},
|
||
LabelPlacement{X: x - radius, Y: y, Anchor: current},
|
||
LabelPlacement{X: x + radius, Y: y - radius, Anchor: current},
|
||
LabelPlacement{X: x - radius, Y: y - radius, Anchor: current},
|
||
LabelPlacement{X: x + radius, Y: y + radius, Anchor: current},
|
||
LabelPlacement{X: x - radius, Y: y + radius, Anchor: current},
|
||
)
|
||
}
|
||
return candidates
|
||
}
|