feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user