feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
|
||||
"b612.me/astro/internal/svgchart"
|
||||
)
|
||||
|
||||
// occultationLabelRect 是画布坐标下的标签占位矩形。
|
||||
type occultationLabelRect struct {
|
||||
left, top, right, bottom float64
|
||||
}
|
||||
|
||||
func (rect occultationLabelRect) overlaps(other occultationLabelRect) bool {
|
||||
return rect.left < other.right && other.left < rect.right && rect.top < other.bottom && other.top < rect.bottom
|
||||
}
|
||||
|
||||
func (rect occultationLabelRect) expanded(padding float64) occultationLabelRect {
|
||||
return occultationLabelRect{
|
||||
left: rect.left - padding, top: rect.top - padding,
|
||||
right: rect.right + padding, bottom: rect.bottom + padding,
|
||||
}
|
||||
}
|
||||
|
||||
// occultationLabelPlacer 是三族地图标签(等时线、中心线时刻、事件标记)共用的已放置表;
|
||||
// nil 表示不做避让。
|
||||
type occultationLabelPlacer struct {
|
||||
placed []occultationLabelRect
|
||||
}
|
||||
|
||||
func (placer *occultationLabelPlacer) reserved(rect occultationLabelRect) bool {
|
||||
if placer == nil {
|
||||
return false
|
||||
}
|
||||
for _, current := range placer.placed {
|
||||
if rect.overlaps(current) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// place 在占位可用时登记并返回 true;padding 是标签之间要求的最小空隙。
|
||||
func (placer *occultationLabelPlacer) place(x, y float64, anchor, text string, fontSize, padding float64) bool {
|
||||
rect := occultationLabelRectFor(x, y, anchor, text, fontSize).expanded(padding)
|
||||
if placer.reserved(rect) {
|
||||
return false
|
||||
}
|
||||
placer.reserve(rect)
|
||||
return true
|
||||
}
|
||||
|
||||
func (placer *occultationLabelPlacer) reserve(rect occultationLabelRect) {
|
||||
if placer == nil {
|
||||
return
|
||||
}
|
||||
placer.placed = append(placer.placed, rect)
|
||||
}
|
||||
|
||||
// occultationLabelTextWidth 估算标签宽度,口径与数据块的压叠判定一致。
|
||||
func occultationLabelTextWidth(text string, fontSize float64) float64 {
|
||||
return svgchart.EstimatedTextWidth(text, fontSize)
|
||||
}
|
||||
|
||||
// occultationLabelRectFor 返回锚点为 (x, y) 的文本占位,上下留白与数据块同口径。
|
||||
func occultationLabelRectFor(x, y float64, anchor, text string, fontSize float64) occultationLabelRect {
|
||||
width := occultationLabelTextWidth(text, fontSize)
|
||||
above, below := svgchart.EstimatedTextExtents(fontSize)
|
||||
left := x
|
||||
switch anchor {
|
||||
case "middle":
|
||||
left = x - width/2
|
||||
case "end":
|
||||
left = x - width
|
||||
}
|
||||
return occultationLabelRect{
|
||||
left: left, top: y - above, right: left + width, bottom: y + below,
|
||||
}
|
||||
}
|
||||
|
||||
// occultationLegendItem 是图例中的一项:色块(fill)与线段(color)至少有一个。
|
||||
type occultationLegendItem struct {
|
||||
label string
|
||||
color string
|
||||
dash string
|
||||
markerWidth float64
|
||||
fill string
|
||||
fillOpacity float64
|
||||
}
|
||||
|
||||
const (
|
||||
occultationLegendRowHeight = 18.0
|
||||
occultationLegendMinimumFontSize = 7.0
|
||||
)
|
||||
|
||||
func occultationLegendItemWidth(item occultationLegendItem, fontSize float64) float64 {
|
||||
return item.markerWidth + 6 + occultationLabelTextWidth(item.label, fontSize)
|
||||
}
|
||||
|
||||
// occultationLegendItemRows 按画布宽度把图例项贪心排成多行;行数不得超过布局预留的两行。
|
||||
func occultationLegendItemRows(
|
||||
layout starOccultationSVGLayout,
|
||||
items []occultationLegendItem,
|
||||
fontSize float64,
|
||||
) [][]occultationLegendItem {
|
||||
rows := make([][]occultationLegendItem, 0, 2)
|
||||
current := make([]occultationLegendItem, 0, len(items))
|
||||
width := 0.0
|
||||
for _, item := range items {
|
||||
itemWidth := occultationLegendItemWidth(item, fontSize)
|
||||
// 布局只预留两行,第三行会压到页脚;超出的项并回第二行而不是换行。
|
||||
if len(current) > 0 && len(rows) < 1 && width+itemWidth > layout.mapWidth {
|
||||
rows = append(rows, current)
|
||||
current = make([]occultationLegendItem, 0, len(items))
|
||||
width = 0
|
||||
}
|
||||
current = append(current, item)
|
||||
width += itemWidth
|
||||
}
|
||||
if len(current) > 0 {
|
||||
rows = append(rows, current)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// occultationLegendFontSize 在放得下的前提下取最大字号:窄地图(详细版式)必须缩字号,
|
||||
// 否则图例项会溢出地图横向范围压到右侧数据块上。
|
||||
func occultationLegendFontSize(layout starOccultationSVGLayout, items []occultationLegendItem, fontSize float64) float64 {
|
||||
for candidate := fontSize; candidate >= occultationLegendMinimumFontSize; candidate-- {
|
||||
rows := occultationLegendItemRows(layout, items, candidate)
|
||||
fits := true
|
||||
for _, row := range rows {
|
||||
total := 0.0
|
||||
for _, item := range row {
|
||||
total += occultationLegendItemWidth(item, candidate)
|
||||
}
|
||||
if total > layout.mapWidth {
|
||||
fits = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if fits {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return occultationLegendMinimumFontSize
|
||||
}
|
||||
|
||||
// writeOccultationLegendItems 逐行写图例;每行内的项按剩余空间均分间隔,避免文字互相压叠。
|
||||
func writeOccultationLegendItems(
|
||||
b *strings.Builder,
|
||||
top float64,
|
||||
layout starOccultationSVGLayout,
|
||||
items []occultationLegendItem,
|
||||
fontSize float64,
|
||||
) {
|
||||
fontSize = occultationLegendFontSize(layout, items, fontSize)
|
||||
rows := occultationLegendItemRows(layout, items, fontSize)
|
||||
y := top
|
||||
for _, row := range rows {
|
||||
total := 0.0
|
||||
for _, item := range row {
|
||||
total += occultationLegendItemWidth(item, fontSize)
|
||||
}
|
||||
gap := 0.0
|
||||
if len(row) > 0 && total < layout.mapWidth {
|
||||
gap = (layout.mapWidth - total) / float64(len(row))
|
||||
}
|
||||
x := layout.mapX
|
||||
for _, item := range row {
|
||||
if item.fill != "" {
|
||||
fmt.Fprintf(b, `<rect x="%.3f" y="%.3f" width="%.3f" height="9" fill="%s" fill-opacity="%.2f"/>`,
|
||||
x, y-10, item.markerWidth, item.fill, item.fillOpacity)
|
||||
}
|
||||
if item.color != "" {
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="%s" stroke-width="2"`,
|
||||
x, y-5, x+item.markerWidth, y-5, item.color)
|
||||
if item.dash != "" {
|
||||
fmt.Fprintf(b, ` stroke-dasharray="%s"`, item.dash)
|
||||
}
|
||||
b.WriteString(`/>`)
|
||||
}
|
||||
fmt.Fprintf(b, `<text class="occultation-legend-text" x="%.3f" y="%.3f" fill="#465053" font-family="Arial, sans-serif" font-size="%.0f">%s</text>`,
|
||||
x+item.markerWidth+6, y, fontSize, html.EscapeString(item.label))
|
||||
x += occultationLegendItemWidth(item, fontSize) + gap
|
||||
}
|
||||
y += occultationLegendRowHeight
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user