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

181 lines
6.3 KiB
Go

package svg
import (
"strconv"
"strings"
"b612.me/astro/internal/geodata"
"b612.me/astro/moon"
)
// occultationScreenPoint 是投影到 SVG 画布后的像素坐标。
type occultationScreenPoint struct {
x float64
y float64
}
// occultationGeometryTolerancePixels 是掩带几何写出前的折线简化容差(像素);0 表示不简化。
var occultationGeometryTolerancePixels = 0.4
// projectOccultationRing 投影并简化一个地理环。
func (layout starOccultationSVGLayout) projectOccultationRing(points []starOccultationGeoPoint) []occultationScreenPoint {
screen := make([]occultationScreenPoint, len(points))
for index, point := range points {
x, y := layout.project(point.longitude, point.latitude)
screen[index] = occultationScreenPoint{x: x, y: y}
}
return simplifyOccultationRing(screen, occultationGeometryTolerancePixels)
}
// projectOccultationPolyline 投影并简化一条地理折线。
func (layout starOccultationSVGLayout) projectOccultationPolyline(points []starOccultationGeoPoint) []occultationScreenPoint {
screen := make([]occultationScreenPoint, len(points))
for index, point := range points {
x, y := layout.project(point.longitude, point.latitude)
screen[index] = occultationScreenPoint{x: x, y: y}
}
return simplifyOccultationPolyline(screen, occultationGeometryTolerancePixels)
}
// projectOccultationPathLine 投影并简化一条月掩路径折线。
func (layout starOccultationSVGLayout) projectOccultationPathLine(points []moon.OccultationPathPoint) []occultationScreenPoint {
screen := make([]occultationScreenPoint, len(points))
for index, point := range points {
x, y := layout.project(point.Longitude, point.Latitude)
screen[index] = occultationScreenPoint{x: x, y: y}
}
return simplifyOccultationPolyline(screen, occultationGeometryTolerancePixels)
}
// projectOccultationGeoRing 投影并简化一条地图多边形环。
func (layout starOccultationSVGLayout) projectOccultationGeoRing(points []geodata.GeoPoint) []occultationScreenPoint {
screen := make([]occultationScreenPoint, len(points))
for index, point := range points {
x, y := layout.project(point.Longitude, point.Latitude)
screen[index] = occultationScreenPoint{x: x, y: y}
}
return simplifyOccultationRing(screen, occultationGeometryTolerancePixels)
}
// simplifyOccultationRing 简化闭合环:首点与离首点最远的顶点保留为两个锚点,两段各自简化。
// 简化后不足三个顶点时返回原环,保证结果仍可填充。
func simplifyOccultationRing(points []occultationScreenPoint, tolerance float64) []occultationScreenPoint {
if len(points) < 4 || !(tolerance > 0) {
return points
}
ring := points
closed := points[0] == points[len(points)-1]
if closed {
ring = points[:len(points)-1]
}
if len(ring) < 3 {
return points
}
anchor := 1
farthest := -1.0
for index := 1; index < len(ring); index++ {
distance := occultationSquaredDistance(ring[0], ring[index])
if distance > farthest {
farthest, anchor = distance, index
}
}
first := simplifyOccultationPolyline(ring[:anchor+1], tolerance)
second := make([]occultationScreenPoint, len(ring)-anchor+1)
copy(second, ring[anchor:])
second[len(second)-1] = ring[0]
second = simplifyOccultationPolyline(second, tolerance)
result := make([]occultationScreenPoint, 0, len(first)+len(second)-2)
result = append(result, first[:len(first)-1]...)
result = append(result, second[:len(second)-1]...)
if len(result) < 3 {
return points
}
if closed {
result = append(result, result[0])
}
return result
}
// simplifyOccultationPolyline 用 Douglas-Peucker 简化折线,端点恒保留。
func simplifyOccultationPolyline(points []occultationScreenPoint, tolerance float64) []occultationScreenPoint {
if len(points) < 3 || !(tolerance > 0) {
return points
}
keep := make([]bool, len(points))
keep[0], keep[len(points)-1] = true, true
toleranceSquared := tolerance * tolerance
type span struct{ first, last int }
stack := make([]span, 0, 16)
stack = append(stack, span{first: 0, last: len(points) - 1})
for len(stack) > 0 {
current := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if current.last <= current.first+1 {
continue
}
index, distance := occultationFarthestVertex(points, current.first, current.last)
if distance <= toleranceSquared {
continue
}
keep[index] = true
stack = append(stack, span{first: current.first, last: index}, span{first: index, last: current.last})
}
result := make([]occultationScreenPoint, 0, len(points))
for index, point := range points {
if keep[index] {
result = append(result, point)
}
}
return result
}
// occultationFarthestVertex 返回首末顶点之间离弦最远的顶点及其平方距离。
func occultationFarthestVertex(points []occultationScreenPoint, first, last int) (int, float64) {
best, bestDistance := first, -1.0
start, end := points[first], points[last]
for index := first + 1; index < last; index++ {
distance := occultationChordSquaredDistance(points[index], start, end)
if distance > bestDistance {
best, bestDistance = index, distance
}
}
return best, bestDistance
}
// occultationChordSquaredDistance 返回点到线段的最短平方距离;线段退化时退化为点距。
func occultationChordSquaredDistance(point, start, end occultationScreenPoint) float64 {
dx, dy := end.x-start.x, end.y-start.y
lengthSquared := dx*dx + dy*dy
if lengthSquared <= 0 {
return occultationSquaredDistance(point, start)
}
position := ((point.x-start.x)*dx + (point.y-start.y)*dy) / lengthSquared
if position < 0 {
position = 0
} else if position > 1 {
position = 1
}
return occultationSquaredDistance(point, occultationScreenPoint{x: start.x + position*dx, y: start.y + position*dy})
}
func occultationSquaredDistance(first, second occultationScreenPoint) float64 {
dx, dy := first.x-second.x, first.y-second.y
return dx*dx + dy*dy
}
// writeOccultationPathCommands 写出 `M x y L x y ...` 命令序列,坐标保留三位小数。
func writeOccultationPathCommands(b *strings.Builder, points []occultationScreenPoint) {
var xBuffer, yBuffer [32]byte
for index, point := range points {
if index == 0 {
b.WriteString("M ")
} else {
b.WriteString("L ")
}
b.Write(strconv.AppendFloat(xBuffer[:0], point.x, 'f', 3, 64))
b.WriteByte(' ')
b.Write(strconv.AppendFloat(yBuffer[:0], point.y, 'f', 3, 64))
b.WriteByte(' ')
}
}