feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPlanetOccultationSaturnFootprintsContainCenterLine(t *testing.T) {
|
||||
config, ok := planetOccultationConfigFor(OccultationSaturn)
|
||||
if !ok {
|
||||
t.Fatal("Saturn occultation config is unavailable")
|
||||
}
|
||||
seedTT := occultationTimeToTT(time.Date(2025, time.February, 1, 4, 0, 48, 0, time.UTC))
|
||||
for _, contact := range []struct {
|
||||
name string
|
||||
frameAt occultationPathFrameFunc
|
||||
}{
|
||||
{name: "partial", frameAt: func(tt float64) (occultationPathFrame, bool) {
|
||||
return planetOccultationPathFrameAt(tt, config)
|
||||
}},
|
||||
{name: "total", frameAt: func(tt float64) (occultationPathFrame, bool) {
|
||||
return planetOccultationTotalPathFrameAt(tt, config)
|
||||
}},
|
||||
} {
|
||||
startTT, endTT, found := occultationPathWindowForFrame(
|
||||
seedTT, seedTT-occultationPathSearchSpanDays, seedTT+occultationPathSearchSpanDays,
|
||||
contact.frameAt, true,
|
||||
)
|
||||
if !found {
|
||||
t.Fatalf("%s center interval is unavailable", contact.name)
|
||||
}
|
||||
checked := 0
|
||||
for tt := startTT + 2.0/1440.0; tt < endTT-2.0/1440.0; tt += 5.0 / 1440.0 {
|
||||
frame, frameOK := contact.frameAt(tt)
|
||||
center, _, centerOK := occultationEarthLineIntersection(frame.moon, frame.axis)
|
||||
footprint, footprintOK := planetOccultationFootprintAt(tt, contact.frameAt, time.UTC)
|
||||
if !frameOK || !centerOK || !footprintOK {
|
||||
t.Fatalf("%s center or footprint is unavailable at TT %.9f", contact.name, tt)
|
||||
}
|
||||
longitude, latitude := occultationPathGeodetic(tt, center)
|
||||
if !planetOccultationFootprintContains(footprint, longitude, latitude) {
|
||||
t.Fatalf("%s footprint does not contain center %.6f, %.6f at TT %.9f",
|
||||
contact.name, longitude, latitude, tt)
|
||||
}
|
||||
checked++
|
||||
}
|
||||
if checked < 10 {
|
||||
t.Fatalf("%s checked only %d center samples", contact.name, checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetOccultationPathRejectsExcessiveAggregateSampling(t *testing.T) {
|
||||
start := time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC)
|
||||
_, err := FindPlanetOccultationPaths(
|
||||
start, start.Add(24*time.Hour), OccultationSaturn,
|
||||
OccultationPathOptions{Step: time.Second},
|
||||
)
|
||||
if !errors.Is(err, ErrOccultationPathSamplingLimit) {
|
||||
t.Fatalf("FindPlanetOccultationPaths() error = %v, want ErrOccultationPathSamplingLimit", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetOccultationFootprintsHaveIndependentSampleBudget(t *testing.T) {
|
||||
times := occultationPathSampleTimesWithLimit(
|
||||
0, 1, 0.500001, 1.0/86400.0, planetOccultationFootprintMaxSamples,
|
||||
)
|
||||
if len(times) > planetOccultationFootprintMaxSamples {
|
||||
t.Fatalf("footprint sample count = %d, maximum %d", len(times), planetOccultationFootprintMaxSamples)
|
||||
}
|
||||
foundGreatest := false
|
||||
for _, sample := range times {
|
||||
if sample == 0.500001 {
|
||||
foundGreatest = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundGreatest {
|
||||
t.Fatal("bounded footprint samples omitted greatest")
|
||||
}
|
||||
}
|
||||
|
||||
func planetOccultationFootprintContains(
|
||||
footprint PlanetOccultationFootprint,
|
||||
longitude, latitude float64,
|
||||
) bool {
|
||||
for _, polygon := range footprint.Polygons {
|
||||
inside := false
|
||||
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||||
currentX := math.Remainder(polygon[current].Longitude-longitude, 360)
|
||||
previousX := math.Remainder(polygon[previous].Longitude-longitude, 360)
|
||||
currentY := polygon[current].Latitude
|
||||
previousY := polygon[previous].Latitude
|
||||
if math.Abs(currentX-previousX) > 180 {
|
||||
if currentX < previousX {
|
||||
currentX += 360
|
||||
} else {
|
||||
previousX += 360
|
||||
}
|
||||
}
|
||||
if (currentY > latitude) == (previousY > latitude) {
|
||||
continue
|
||||
}
|
||||
intersectionX := previousX + (latitude-previousY)*(currentX-previousX)/(currentY-previousY)
|
||||
if intersectionX > 0 {
|
||||
inside = !inside
|
||||
}
|
||||
}
|
||||
if inside {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user