Files
astro/basic/occultation_planet_footprint.go
T
b612 9ee2163cc7 feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算
- 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出
- 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口
- 扩展日食中心线、南北界及偏食足迹采样,支持极区投影
- 修正站心时角、月出月落、月球视半径、折射和恒星自行计算
- 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
2026-08-06 12:00:56 +08:00

189 lines
5.3 KiB
Go

package basic
import (
"math"
"time"
)
const (
planetOccultationFootprintBoundaryPoints = 180
planetOccultationHorizonPoints = 360
planetOccultationFootprintMaxSamples = 360
)
type planetOccultationFootprintSample struct {
point OccultationPathPoint
ok bool
}
func planetOccultationFootprints(
startTT, endTT, greatestTT float64,
frameAt occultationPathFrameFunc,
options OccultationPathOptions,
location *time.Location,
) []PlanetOccultationFootprint {
stepDays := float64(options.Step) / float64(24*time.Hour)
times := occultationPathSampleTimesWithLimit(
startTT, endTT, greatestTT, stepDays, planetOccultationFootprintMaxSamples,
)
footprints := make([]PlanetOccultationFootprint, 0, len(times))
for _, tt := range times {
footprint, ok := planetOccultationFootprintAt(tt, frameAt, location)
if ok {
footprints = append(footprints, footprint)
}
}
return footprints
}
func planetOccultationFootprintAt(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
) (PlanetOccultationFootprint, bool) {
frame, ok := frameAt(tt)
if !ok {
return PlanetOccultationFootprint{}, false
}
samples := make([]planetOccultationFootprintSample, planetOccultationFootprintBoundaryPoints)
for index := range samples {
theta := 2 * math.Pi * float64(index) / float64(len(samples))
vector, _, valid := occultationPathBoundaryVector(frame, theta)
if valid {
samples[index] = planetOccultationFootprintSample{
point: occultationPathPointFromVector(tt, vector, 0, location),
ok: true,
}
}
}
segments, closed := planetOccultationFootprintSegments(samples)
polygons := make([][]OccultationPathPoint, 0, len(segments))
for _, segment := range segments {
if len(segment) < 2 {
continue
}
polygon := append([]OccultationPathPoint(nil), segment...)
if closed {
polygon = append(polygon, polygon[0])
} else {
polygon = append(polygon, planetOccultationHorizonArc(tt, frame, segment[len(segment)-1], segment[0], location)...)
}
if len(polygon) >= 4 {
polygons = append(polygons, polygon)
}
}
if len(polygons) == 0 {
return PlanetOccultationFootprint{}, false
}
return PlanetOccultationFootprint{
Time: occultationTTToLocation(tt, location),
Polygons: polygons,
}, true
}
func planetOccultationFootprintSegments(
samples []planetOccultationFootprintSample,
) ([][]OccultationPathPoint, bool) {
segments := make([][]OccultationPathPoint, 0, 2)
current := make([]OccultationPathPoint, 0, len(samples))
allValid := len(samples) > 0
for _, sample := range samples {
if !sample.ok {
allValid = false
if len(current) > 0 {
segments = append(segments, current)
current = nil
}
continue
}
current = append(current, sample.point)
}
if len(current) > 0 {
segments = append(segments, current)
}
if len(segments) > 1 && samples[0].ok && samples[len(samples)-1].ok {
first := segments[0]
last := segments[len(segments)-1]
merged := make([]OccultationPathPoint, 0, len(last)+len(first))
merged = append(merged, last...)
merged = append(merged, first...)
segments[0] = merged
segments = segments[:len(segments)-1]
}
return segments, allValid && len(segments) == 1
}
func planetOccultationHorizonArc(
tt float64,
frame occultationPathFrame,
from, to OccultationPathPoint,
location *time.Location,
) []OccultationPathPoint {
sublunarLongitude, sublunarLatitude := occultationPathGeodetic(tt, frame.moon)
circle := planetOccultationSphericalCircle(
occultationTTToLocation(tt, location), sublunarLongitude, sublunarLatitude,
90, planetOccultationHorizonPoints,
)
fromIndex := planetOccultationNearestPointIndex(circle, from)
toIndex := planetOccultationNearestPointIndex(circle, to)
forwardSteps := (toIndex - fromIndex + len(circle)) % len(circle)
backwardSteps := (fromIndex - toIndex + len(circle)) % len(circle)
direction := 1
steps := forwardSteps
if backwardSteps < forwardSteps {
direction = -1
steps = backwardSteps
}
arc := make([]OccultationPathPoint, 0, steps+1)
for step := 1; step < steps; step++ {
index := (fromIndex + direction*step) % len(circle)
if index < 0 {
index += len(circle)
}
arc = append(arc, circle[index])
}
return append(arc, to)
}
func planetOccultationSphericalCircle(
value time.Time,
centerLongitude, centerLatitude, radius float64,
count int,
) []OccultationPathPoint {
centerLongitude *= math.Pi / 180
centerLatitude *= math.Pi / 180
radius *= math.Pi / 180
points := make([]OccultationPathPoint, count)
for index := range points {
bearing := 2 * math.Pi * float64(index) / float64(count)
latitude := math.Asin(
math.Sin(centerLatitude)*math.Cos(radius) +
math.Cos(centerLatitude)*math.Sin(radius)*math.Cos(bearing),
)
longitude := centerLongitude + math.Atan2(
math.Sin(bearing)*math.Sin(radius)*math.Cos(centerLatitude),
math.Cos(radius)-math.Sin(centerLatitude)*math.Sin(latitude),
)
points[index] = OccultationPathPoint{
Time: value,
Longitude: normalizeLongitude(longitude * 180 / math.Pi),
Latitude: latitude * 180 / math.Pi,
}
}
return points
}
func planetOccultationNearestPointIndex(points []OccultationPathPoint, target OccultationPathPoint) int {
nearest := 0
distance := math.Inf(1)
for index, point := range points {
candidate := occultationPathDistanceKM(point, target)
if candidate < distance {
nearest = index
distance = candidate
}
}
return nearest
}