feat: 完善水星与金星全球凌日搜索及制图导出
- 基于平均下合周期与节点相位预筛候选,减少无效星历计算;跳过无法求解的单个会合周期,避免有限覆盖星历导致搜索提前终止 - 引入影轴 γ、半影半径及地球尺度几何判定,结合站心可见性识别地心无接触、仅部分地区可见的掠凌 - 添加金星 243 年、水星 46 年周期族编号,并明确编号适用范围及未分配时的零值语义 - 为 SVG、GeoJSON 和 KML 增加站心部分凌日可见区,适配接触等时线、几何元数据、图层样式及取景;为地心无接触凌日事件增加正射图,按可见区球面面积加权居中
This commit is contained in:
41 files changed
+1554
-307
No files matched your search
@@ -0,0 +1,170 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/internal/ephem"
|
||||
)
|
||||
|
||||
func TestPlanetTransitGlobalGrazes(tester *testing.T) {
|
||||
for _, model := range []ephem.Model{ephem.Semianalytic, ephem.Legacy} {
|
||||
inst := ephem.NewModelInstance(model)
|
||||
for _, year := range []int{-540, 2854} {
|
||||
transit := NextVenusTransitWithInstance(JDCalc(year, 1, 1), inst)
|
||||
if !transit.Valid || JD2DateByZone(transit.Greatest, time.UTC, false).Year() != year {
|
||||
tester.Fatalf("model=%d year=%d: missed global event: %+v", model, year, transit)
|
||||
}
|
||||
if transit.HasExternal || transit.HasInternal || transit.ExternalIngress != 0 || transit.ExternalEgress != 0 {
|
||||
tester.Fatalf("global graze fabricated geocentric contacts: %+v", transit)
|
||||
}
|
||||
if math.Abs(transit.Gamma) <= transit.PenumbralRadius || math.Abs(transit.Gamma) >= transit.PenumbralRadius+1.01 {
|
||||
tester.Fatalf("unexpected shadow geometry: %+v", transit)
|
||||
}
|
||||
observer := Observer{Longitude: -135, Latitude: -60}
|
||||
local := PlanetTransitLocalWithInstance(transit, observer, inst)
|
||||
if !local.Valid || !local.Visible || local.HasInternal || !local.ExternalIngress.Valid || !local.ExternalEgress.Valid {
|
||||
tester.Fatalf("model=%d year=%d: missing visible local graze: %+v", model, year, local)
|
||||
}
|
||||
if year == 2854 {
|
||||
if other := PlanetTransitLocalWithInstance(transit, Observer{Latitude: 45}, inst); other.Valid {
|
||||
tester.Fatal("northern site must miss the 2854 event")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetTransitSeriesNumbers(tester *testing.T) {
|
||||
for _, sample := range []struct{ planet, year, number, period int }{
|
||||
{2, 1631, 6, 243}, {2, 1639, 4, 243}, {2, 2004, 3, 243}, {2, 2012, 5, 243}, {2, 2611, 4, 243}, {2, 2854, 4, 243},
|
||||
{1, 1957, 9, 46}, {1, 2003, 9, 46}, {1, 2006, 8, 46}, {1, 2016, 7, 46}, {1, 2019, 6, 46}, {1, 2174, 11, 46},
|
||||
} {
|
||||
query := JDCalc(sample.year, 1, 1)
|
||||
result := NextVenusTransit(query)
|
||||
if sample.planet == 1 {
|
||||
result = NextMercuryTransit(query)
|
||||
}
|
||||
if result.Series.Number != sample.number || result.Series.PeriodYears != sample.period {
|
||||
tester.Fatalf("planet=%d year=%d: series=%+v, want %d/%d", sample.planet, sample.year, result.Series, sample.number, sample.period)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetTransitSeriesUnassigned(tester *testing.T) {
|
||||
for _, sample := range []struct {
|
||||
cfg planetTransitConfig
|
||||
year int
|
||||
}{
|
||||
{mercuryTransitConfig(), 1500}, {mercuryTransitConfig(), 2400},
|
||||
{venusTransitConfig(), -2100}, {venusTransitConfig(), 4100},
|
||||
} {
|
||||
series := planetTransitSeriesAt(JDCalc(sample.year, 1, 1), sample.cfg)
|
||||
if series.Number != 0 || series.PeriodYears == 0 {
|
||||
tester.Fatalf("unassigned series: %+v", series)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetTransitNearInternalEdge(tester *testing.T) {
|
||||
for _, year := range []int{1631, 2611} {
|
||||
transit := NextVenusTransit(JDCalc(year, 1, 1))
|
||||
if !transit.Valid || !transit.HasExternal || !transit.HasInternal {
|
||||
tester.Fatalf("year=%d: missing geocentric contacts", year)
|
||||
}
|
||||
full, partial := false, false
|
||||
for latitude := -75; latitude <= 75; latitude += 30 {
|
||||
for longitude := -180; longitude < 180; longitude += 45 {
|
||||
local := PlanetTransitLocal(transit, Observer{Longitude: float64(longitude), Latitude: float64(latitude)})
|
||||
if !local.Valid || !local.Visible {
|
||||
continue
|
||||
}
|
||||
if local.HasInternal {
|
||||
full = true
|
||||
} else {
|
||||
partial = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !full || !partial {
|
||||
tester.Fatalf("year=%d: full=%v partial=%v", year, full, partial)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetTransitInputBounds(tester *testing.T) {
|
||||
for _, query := range []float64{math.NaN(), math.Inf(1), math.Inf(-1), math.MaxFloat64} {
|
||||
if result := NextVenusTransit(query); result.Valid {
|
||||
tester.Fatal("non-finite query produced a transit")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetTransitResultRebuild(tester *testing.T) {
|
||||
greatest := time.Date(2854, 12, 14, 13, 0, 0, 0, time.UTC)
|
||||
if PlanetTransitResultFromTimes(2, time.Time{}, time.Time{}, greatest, time.Time{}, time.Time{}).Valid {
|
||||
tester.Fatal("contact rebuild accepted missing C1/C4")
|
||||
}
|
||||
for _, window := range [][2]time.Time{
|
||||
{{}, greatest.Add(time.Hour)}, {greatest, greatest.Add(time.Hour)}, {greatest.Add(-time.Hour), greatest},
|
||||
} {
|
||||
if PlanetTransitResultFromWindow(2, window[0], greatest, window[1]).Valid {
|
||||
tester.Fatalf("window rebuild accepted %v", window)
|
||||
}
|
||||
}
|
||||
transit := NextVenusTransit(JDCalc(2854, 1, 1))
|
||||
rebuilt := PlanetTransitResultFromWindow(2, JD2DateByZone(transit.WindowStart, time.UTC, false),
|
||||
JD2DateByZone(transit.Greatest, time.UTC, false), JD2DateByZone(transit.WindowEnd, time.UTC, false))
|
||||
observer := Observer{Longitude: -135, Latitude: -60}
|
||||
want, got := PlanetTransitLocal(transit, observer), PlanetTransitLocal(rebuilt, observer)
|
||||
if !got.Valid || math.Abs(got.ExternalIngress.JD-want.ExternalIngress.JD)*86400 > 0.01 || math.Abs(got.ExternalEgress.JD-want.ExternalEgress.JD)*86400 > 0.01 {
|
||||
tester.Fatalf("window rebuild changed local contacts: got %+v want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// boundedCoverageEngine 把内置星历限制在一个时间窗内,窗外的请求按越界报错。
|
||||
type boundedCoverageEngine struct {
|
||||
ephem.Engine
|
||||
start, end float64
|
||||
}
|
||||
|
||||
func (bounded boundedCoverageEngine) within(jd float64) bool {
|
||||
return jd >= bounded.start && jd <= bounded.end
|
||||
}
|
||||
|
||||
func (bounded boundedCoverageEngine) State(target, center ephem.Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if !bounded.within(jdTDB) {
|
||||
return [3]float64{}, [3]float64{}, ephem.ErrOutOfRange
|
||||
}
|
||||
return bounded.Engine.State(target, center, jdTDB)
|
||||
}
|
||||
|
||||
func (bounded boundedCoverageEngine) Apparent(target ephem.Body, jdTDB float64) (pos [3]float64, lightTime, distance float64, err error) {
|
||||
if !bounded.within(jdTDB) {
|
||||
return [3]float64{}, 0, 0, ephem.ErrOutOfRange
|
||||
}
|
||||
return bounded.Engine.(ephem.ApparentEngine).Apparent(target, jdTDB)
|
||||
}
|
||||
|
||||
func (bounded boundedCoverageEngine) Coverage(ephem.Body) (start, end float64, ok bool) {
|
||||
return bounded.start, bounded.end, true
|
||||
}
|
||||
|
||||
// 有限覆盖星历:搜索起点前的候选落在覆盖之外时,不得因此丢掉覆盖内的凌日。
|
||||
func TestPlanetTransitBoundedEphemerisCoverage(tester *testing.T) {
|
||||
base := ephem.NewModelInstance(ephem.Semianalytic).Engine()
|
||||
start := JDCalc(2019, 1, 1)
|
||||
bounded := ephem.NewInstance(boundedCoverageEngine{Engine: base, start: start, end: JDCalc(2021, 1, 1)})
|
||||
query := start + 60
|
||||
transit := NextMercuryTransitWithInstance(query, bounded)
|
||||
if !transit.Valid {
|
||||
tester.Fatal("bounded ephemeris lost the 2019-11-11 transit inside its coverage")
|
||||
}
|
||||
if year := JD2DateByZone(transit.Greatest, time.UTC, false).Year(); year != 2019 {
|
||||
tester.Fatalf("bounded ephemeris returned year %d, want 2019", year)
|
||||
}
|
||||
if transit.HasExternal != true || transit.HasInternal != true {
|
||||
tester.Fatalf("bounded ephemeris returned an incomplete transit: %+v", transit)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user