feat: 完善水星与金星全球凌日搜索及制图导出

- 基于平均下合周期与节点相位预筛候选,减少无效星历计算;跳过无法求解的单个会合周期,避免有限覆盖星历导致搜索提前终止
- 引入影轴 γ、半影半径及地球尺度几何判定,结合站心可见性识别地心无接触、仅部分地区可见的掠凌
- 添加金星 243 年、水星 46 年周期族编号,并明确编号适用范围及未分配时的零值语义
- 为 SVG、GeoJSON 和 KML 增加站心部分凌日可见区,适配接触等时线、几何元数据、图层样式及取景;为地心无接触凌日事件增加正射图,按可见区球面面积加权居中
This commit is contained in:
b612 committed 2026-10-20 22:35:39 +08:00
1 parent 72460fa228
commit ccf5880269
41 files changed
+1554 -307

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
package svg
import (
"fmt"
"math"
"strings"
"b612.me/astro/basic"
"b612.me/astro/internal/svgmap"
)
func transitMapOrthographicCenter(transit basic.PlanetTransitResult) svgmap.GeoPoint {
if !transit.HasExternal {
if center, ok := transitMapVisibilityCenter(basic.PlanetTransitVisibilityColumns(transit)); ok {
return center
}
}
return solarEclipseSubsolarPoint(transitMapContactsOf(transit).greatest)
}
func transitMapVisibilityCenter(columns []basic.PlanetTransitVisibilityColumn) (svgmap.GeoPoint, bool) {
var momentX, momentY, momentZ float64
for index, column := range columns {
weight := 0.0
if index > 0 {
weight += (column.Longitude - columns[index-1].Longitude) / 2
}
if index+1 < len(columns) {
weight += (columns[index+1].Longitude - column.Longitude) / 2
}
longitude := column.Longitude * math.Pi / 180
for _, interval := range column.Intervals {
lower, upper := interval[0]*math.Pi/180, interval[1]*math.Pi/180
horizontalMoment := (upper-lower)/2 + (math.Sin(2*upper)-math.Sin(2*lower))/4
verticalMoment := (math.Sin(upper)*math.Sin(upper) - math.Sin(lower)*math.Sin(lower)) / 2
momentX += weight * horizontalMoment * math.Cos(longitude)
momentY += weight * horizontalMoment * math.Sin(longitude)
momentZ += weight * verticalMoment
}
}
if math.Sqrt(momentX*momentX+momentY*momentY+momentZ*momentZ) < 1e-12 {
return svgmap.GeoPoint{}, false
}
return svgmap.GeoPoint{
Longitude: math.Atan2(momentY, momentX) * 180 / math.Pi,
Latitude: math.Atan2(momentZ, math.Hypot(momentX, momentY)) * 180 / math.Pi,
}, true
}
func transitMapProjectionLabel(projection svgmap.Projection, hasExternal bool, language string) string {
if projection != svgmap.ProjectionOrthographic {
return eclipseMapProjectionLabel(projection, language)
}
if language == "en" {
center := "the subsolar point at greatest"
if !hasExternal {
center = "the topocentric visible footprint"
}
return "Orthographic globe projection, centred on " + center + "; one hemisphere only"
}
center := "凌甚太阳直射点"
if !hasExternal {
center = "站心可见区中心"
}
return "正射球面投影,视点取" + center + ",只画朝向视点的半个地球"
}
func writeTransitMapPartialRegions(builder *strings.Builder, frame svgmap.Frame, transit basic.PlanetTransitResult) {
fmt.Fprintf(builder, `<g class="transit-visibility-regions" data-source="%s" clip-path="url(#transit-map-clip)">`, eclipseMapSourceTransitPartialVisibility)
var path strings.Builder
for _, polygon := range basic.PlanetTransitVisibilityPolygons(transit) {
points := make([]svgmap.GeoPoint, len(polygon))
for index, point := range polygon {
points[index] = svgmap.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude}
}
path.WriteString(lunarEclipseGeoPolygonPath(points, frame))
}
if path.Len() > 0 {
fmt.Fprintf(builder, `<path class="transit-partial-region" d="%s" fill="%s" fill-opacity="%.2f" fill-rule="nonzero"/>`, path.String(), transitMapColorPartial, transitMapPartialFillOpacity)
}
builder.WriteString(`</g>`)
}
func transitMapGeometryNote(transit basic.PlanetTransitResult, language string) string {
text := fmt.Sprintf("γ %.4f", transit.Gamma)
if transit.Series.Number != 0 {
if language == "en" {
text += fmt.Sprintf(" | Series %d (%d yr)", transit.Series.Number, transit.Series.PeriodYears)
} else {
text += fmt.Sprintf(" | 周期族 %d(%d 年)", transit.Series.Number, transit.Series.PeriodYears)
}
}
return text
}