feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
package geodata
|
||||
|
||||
import "math"
|
||||
|
||||
// rad 是度到弧度的换算因子 / rad converts degrees to radians.
|
||||
const rad = math.Pi / 180
|
||||
|
||||
const (
|
||||
// orthographicRimSteps 是视界闭合弧与整盘回退环的加密段数。
|
||||
orthographicRimSteps = 180
|
||||
// orthographicCrossingIterations 是视界交点的二分次数,1e-12 弧度量级足够。
|
||||
orthographicCrossingIterations = 48
|
||||
// orthographicRunCapacity 是单个可见段的初始容量:段长与环长无关,
|
||||
// 按环长预分配会让反复穿越视界的环退化成 O(段数×环长) 的内存。
|
||||
orthographicRunCapacity = 8
|
||||
)
|
||||
|
||||
// OrthographicDiskPoint 把点正射投影到可见半球的单位圆盘,x 向东、y 向北。
|
||||
// 第二个返回值是深度余弦;false 表示点落在背面,不与可见半球构成一一映射。
|
||||
// OrthographicDiskPoint projects a point onto the unit disk of the visible hemisphere, x east and y north.
|
||||
func OrthographicDiskPoint(point, center GeoPoint) (float64, float64, bool) {
|
||||
longitude := (point.Longitude - center.Longitude) * rad
|
||||
latitude := point.Latitude * rad
|
||||
centerLatitude := center.Latitude * rad
|
||||
cosLatitude, sinLatitude := math.Cos(latitude), math.Sin(latitude)
|
||||
sinCenter, cosCenter := math.Sin(centerLatitude), math.Cos(centerLatitude)
|
||||
cosine := sinCenter*sinLatitude + cosCenter*cosLatitude*math.Cos(longitude)
|
||||
// 视界本身(余弦为 0)映射到圆盘边界,必须可投影;只有严格背面才折叠到盘内。
|
||||
if cosine < -1e-9 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return cosLatitude * math.Sin(longitude),
|
||||
cosCenter*sinLatitude - sinCenter*cosLatitude*math.Cos(longitude), true
|
||||
}
|
||||
|
||||
// orthographicDepth 返回点相对视点的深度余弦,正值表示在可见半球上。
|
||||
func orthographicDepth(point, center GeoPoint) float64 {
|
||||
return geoVectorDot(geoPointVector(point), geoPointVector(center))
|
||||
}
|
||||
|
||||
// orthographicCrossing 二分求线段与视界大圆的交点;两端同侧时返回 false。
|
||||
func orthographicCrossing(first, second GeoPoint, center GeoPoint) (GeoPoint, bool) {
|
||||
firstDepth := orthographicDepth(first, center)
|
||||
secondDepth := orthographicDepth(second, center)
|
||||
if firstDepth == 0 {
|
||||
return first, true
|
||||
}
|
||||
if secondDepth == 0 {
|
||||
return second, true
|
||||
}
|
||||
if (firstDepth > 0) == (secondDepth > 0) {
|
||||
return GeoPoint{}, false
|
||||
}
|
||||
// 收敛到起点那一侧的边界:可见性仍与起点相同就往后挪,翻转了就往前收。
|
||||
firstVisible := firstDepth > 0
|
||||
low, high := 0.0, 1.0
|
||||
for iteration := 0; iteration < orthographicCrossingIterations; iteration++ {
|
||||
middle := (low + high) / 2
|
||||
if (orthographicDepth(InterpolateGreatCircle(first, second, middle), center) > 0) == firstVisible {
|
||||
low = middle
|
||||
} else {
|
||||
high = middle
|
||||
}
|
||||
}
|
||||
return InterpolateGreatCircle(first, second, (low+high)/2), true
|
||||
}
|
||||
|
||||
// clipPolylineOrthographic 把折线裁到可见半球,并在视界处插入精确交点。
|
||||
func clipPolylineOrthographic(points []GeoPoint, center GeoPoint) [][]GeoPoint {
|
||||
if len(points) == 0 {
|
||||
return nil
|
||||
}
|
||||
segments := make([][]GeoPoint, 0, 2)
|
||||
current := make([]GeoPoint, 0, len(points))
|
||||
for index, point := range points {
|
||||
if orthographicDepth(point, center) > 0 {
|
||||
if len(current) == 0 && index > 0 {
|
||||
if crossing, ok := orthographicCrossing(points[index-1], point, center); ok {
|
||||
current = append(current, crossing)
|
||||
}
|
||||
}
|
||||
current = append(current, point)
|
||||
continue
|
||||
}
|
||||
if len(current) > 0 {
|
||||
if crossing, ok := orthographicCrossing(points[index-1], point, center); ok {
|
||||
current = append(current, crossing)
|
||||
}
|
||||
if len(current) >= 2 {
|
||||
segments = append(segments, current)
|
||||
}
|
||||
current = nil
|
||||
}
|
||||
}
|
||||
if len(current) >= 2 {
|
||||
segments = append(segments, current)
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
// orthographicRimArc 沿视界大圆从起点加密到终点;long 为 true 时走另一侧的长弧。
|
||||
// 视界大圆的法线就是视点方向,必须绕它旋转:两端接近对径时 cross(起点, 终点) 会退化成零向量,
|
||||
// 那样闭合弧会塌成一条横穿圆盘的直线弦。
|
||||
func orthographicRimArc(from, to GeoPoint, center GeoPoint, long bool) []GeoPoint {
|
||||
axis := geoPointVector(center)
|
||||
startVector, ok := geoVectorNormalize(geoVectorAdd(
|
||||
geoPointVector(from),
|
||||
geoVectorScale(axis, -geoVectorDot(geoPointVector(from), axis)),
|
||||
))
|
||||
if !ok {
|
||||
return []GeoPoint{from, to}
|
||||
}
|
||||
tangent := geoVectorCross(axis, startVector)
|
||||
endVector := geoPointVector(to)
|
||||
signed := math.Atan2(geoVectorDot(endVector, tangent), geoVectorDot(endVector, startVector))
|
||||
begin, span := 0.0, signed
|
||||
if long {
|
||||
turn := 2 * math.Pi
|
||||
if signed < 0 {
|
||||
turn = -2 * math.Pi
|
||||
}
|
||||
begin, span = signed, turn-signed
|
||||
}
|
||||
arc := make([]GeoPoint, 0, orthographicRimSteps+1)
|
||||
for step := 0; step <= orthographicRimSteps; step++ {
|
||||
angle := begin + span*float64(step)/orthographicRimSteps
|
||||
arc = append(arc, geoVectorPoint(geoVectorAdd(
|
||||
geoVectorScale(startVector, math.Cos(angle)),
|
||||
geoVectorScale(tangent, math.Sin(angle)),
|
||||
)))
|
||||
}
|
||||
return arc
|
||||
}
|
||||
|
||||
// orthographicRimInside 判断某段视界弧是否紧邻环的内部:把弧中点朝可见半球内侧挪一点再看它落在哪一侧。
|
||||
func orthographicRimInside(arc []GeoPoint, ring []GeoPoint, center GeoPoint) bool {
|
||||
if len(arc) == 0 {
|
||||
return false
|
||||
}
|
||||
middle := geoPointVector(arc[len(arc)/2])
|
||||
inside := geoVectorAdd(middle, geoVectorScale(geoPointVector(center), 1e-3))
|
||||
probe, ok := geoVectorNormalize(inside)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return sphericalPolygonContainsOrTouches(ring, geoVectorPoint(probe))
|
||||
}
|
||||
|
||||
// closeOrthographicRun 把一段可见折线沿视界大圆闭合回起点,闭合弧取紧邻环内部的那一侧。
|
||||
func closeOrthographicRun(run, ring []GeoPoint, center GeoPoint) []GeoPoint {
|
||||
if len(run) < 2 {
|
||||
return nil
|
||||
}
|
||||
exit, entry := run[len(run)-1], run[0]
|
||||
shortArc := orthographicRimArc(exit, entry, center, false)
|
||||
longArc := orthographicRimArc(exit, entry, center, true)
|
||||
arc := shortArc
|
||||
switch {
|
||||
case orthographicRimInside(shortArc, ring, center):
|
||||
case orthographicRimInside(longArc, ring, center):
|
||||
arc = longArc
|
||||
}
|
||||
closed := make([]GeoPoint, 0, len(run)+len(arc))
|
||||
closed = append(closed, run...)
|
||||
closed = append(closed, arc[1:len(arc)-1]...)
|
||||
return closed
|
||||
}
|
||||
|
||||
// polygonFragmentsOrthographic 把环裁到可见半球,并沿视界大圆闭合被切断的部分。
|
||||
func polygonFragmentsOrthographic(points []GeoPoint, center GeoPoint) [][]GeoPoint {
|
||||
if len(points) < 3 {
|
||||
return nil
|
||||
}
|
||||
visible := 0
|
||||
for _, point := range points {
|
||||
if orthographicDepth(point, center) > 0 {
|
||||
visible++
|
||||
}
|
||||
}
|
||||
if visible == len(points) {
|
||||
return [][]GeoPoint{points}
|
||||
}
|
||||
if visible == 0 {
|
||||
// 整环都在背面:只有把视点包在环内的环,其内部才会覆盖整个可见半球——否则可见部分为空。
|
||||
if sphericalPolygonContainsOrTouches(points, center) {
|
||||
return [][]GeoPoint{SphericalCircle(center, 90, orthographicRimSteps)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 逐边展开成"顶点 + 视界交点"序列,再按可见性切段;闭合环首尾相接,所以按环遍历。
|
||||
type rimNode struct {
|
||||
point GeoPoint
|
||||
visible bool
|
||||
}
|
||||
nodes := make([]rimNode, 0, 2*len(points))
|
||||
for index := 0; index < len(points); index++ {
|
||||
first := points[index]
|
||||
second := points[(index+1)%len(points)]
|
||||
firstVisible := orthographicDepth(first, center) > 0
|
||||
secondVisible := orthographicDepth(second, center) > 0
|
||||
nodes = append(nodes, rimNode{point: first, visible: firstVisible})
|
||||
if firstVisible != secondVisible {
|
||||
if crossing, ok := orthographicCrossing(first, second, center); ok {
|
||||
// 交点落在视界上,两侧的可见段都要以它收尾/起头,所以它恒属于可见段。
|
||||
nodes = append(nodes, rimNode{point: crossing, visible: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
runs := make([][]GeoPoint, 0, 4)
|
||||
current := make([]GeoPoint, 0, orthographicRunCapacity)
|
||||
for _, node := range nodes {
|
||||
if node.visible {
|
||||
current = append(current, node.point)
|
||||
continue
|
||||
}
|
||||
if len(current) >= 2 {
|
||||
runs = append(runs, current)
|
||||
}
|
||||
current = make([]GeoPoint, 0, orthographicRunCapacity)
|
||||
}
|
||||
if len(current) >= 2 {
|
||||
runs = append(runs, current)
|
||||
}
|
||||
// 环首尾相接:起点本身可见时,同一段可见区间会被切成首尾两段,必须先接回来再闭合。
|
||||
if len(runs) >= 2 && nodes[0].visible && nodes[len(nodes)-1].visible {
|
||||
merged := make([]GeoPoint, 0, len(runs[0])+len(runs[len(runs)-1]))
|
||||
merged = append(merged, runs[len(runs)-1]...)
|
||||
merged = append(merged, runs[0]...)
|
||||
runs[0] = merged
|
||||
runs = runs[:len(runs)-1]
|
||||
}
|
||||
fragments := make([][]GeoPoint, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
if closed := closeOrthographicRun(run, points, center); len(closed) >= 3 {
|
||||
fragments = append(fragments, closed)
|
||||
}
|
||||
}
|
||||
return fragments
|
||||
}
|
||||
Reference in New Issue
Block a user