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

84 lines
2.6 KiB
Go

package svg
import (
"fmt"
"html"
"strings"
"b612.me/astro/moon"
)
// writeOccultationGreatestTimeContours 绘制地方掩甚时刻等值线及其 HH:MM 标注。
func writeOccultationGreatestTimeContours(
b *strings.Builder,
contours []moon.OccultationGreatestTimeContour,
layout starOccultationSVGLayout,
options StarOccultationSVGOptions,
placer *occultationLabelPlacer,
) {
if len(contours) == 0 {
return
}
type isochroneLabel struct {
x, y float64
text string
}
labels := make([]isochroneLabel, 0, len(contours))
for _, contour := range contours {
for _, segment := range contour.Segments {
writeStarOccultationGeoLine(b, segment, layout,
occultationLineStyle{className: "greatest-time-isoline", color: "#1f6fb2", strokeWidth: 1.1})
}
text := contour.Time.In(options.Location).Format("15:04")
if x, y, ok := occultationGreatestTimeIsolineLabelPlacement(contour, text, layout, placer); ok {
labels = append(labels, isochroneLabel{x: x, y: y, text: text})
}
}
for _, label := range labels {
fmt.Fprintf(b, `<text class="greatest-time-isoline-label" x="%.3f" y="%.3f" fill="#1a5c96" stroke="#ffffff" stroke-width="2.6" paint-order="stroke" font-family="Arial, sans-serif" font-size="9" font-weight="700" text-anchor="middle">%s</text>`,
label.x, label.y, html.EscapeString(label.text))
}
}
// occultationGreatestTimeIsolineLabelPlacement 在最长的支路上取首个未被其它标签占用的标注点。
func occultationGreatestTimeIsolineLabelPlacement(
contour moon.OccultationGreatestTimeContour,
text string,
layout starOccultationSVGLayout,
placer *occultationLabelPlacer,
) (float64, float64, bool) {
longest := -1
for index, segment := range contour.Segments {
if longest < 0 || len(segment) > len(contour.Segments[longest]) {
longest = index
}
}
if longest < 0 {
return 0, 0, false
}
segment := contour.Segments[longest]
frame := layout.mapFrame()
middle := len(segment) / 2
for offset := 0; offset <= middle; offset++ {
for _, index := range []int{middle + offset, middle - offset} {
if index < 0 || index >= len(segment) {
continue
}
point := segment[index]
x, y, visible := frame.Project(point.Longitude, point.Latitude)
if !visible ||
x < frame.X+30 || x > frame.X+frame.Width-30 ||
y < frame.Y+18 || y > frame.Y+frame.Height-18 {
continue
}
// 优先压在等时线上方,其次下方;两者都被占用就换下一个候选点。
for _, baseline := range []float64{y - 6, y + 13} {
if placer.place(x, baseline, "middle", text, 9, 2) {
return x, baseline, true
}
}
}
}
return 0, 0, false
}