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