2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
184 lines
5.2 KiB
Go
184 lines
5.2 KiB
Go
package geojson_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
"b612.me/astro/eclipse"
|
|
"b612.me/astro/geojson"
|
|
)
|
|
|
|
func TestLunarGeoJSONUsesTopocentricHorizon(t *testing.T) {
|
|
date := time.Date(2026, 3, 3, 0, 0, 0, 0, time.UTC)
|
|
info, ok := eclipse.LunarEclipseOnDate(date)
|
|
if !ok {
|
|
t.Fatal("missing lunar eclipse")
|
|
}
|
|
data, err := geojson.MarshalLunarEclipse(info, 360)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
collection := decodeCollection(t, data)
|
|
for _, contact := range []struct {
|
|
role, horizon string
|
|
at time.Time
|
|
}{
|
|
{"visible-at-p1", "p1-horizon", info.PenumbralStart},
|
|
{"visible-at-p4", "p4-horizon", info.PenumbralEnd},
|
|
} {
|
|
band := featureWithRole(t, collection, contact.role)
|
|
line := featureWithRole(t, collection, contact.horizon)
|
|
var segments [][][]float64
|
|
if err := json.Unmarshal(line.Geometry.Coordinates, &segments); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
jd := basic.Date2JDE(contact.at.UTC())
|
|
for _, segment := range segments {
|
|
for _, point := range segment {
|
|
if math.Abs(point[0]) == 180 {
|
|
continue
|
|
}
|
|
if altitude := basic.HMoonHeight(jd, point[0], point[1], 0); math.Abs(altitude) > 1e-8 {
|
|
t.Fatalf("horizon altitude=%g", altitude)
|
|
}
|
|
}
|
|
}
|
|
for lon := -175.; lon < 180; lon += 10 {
|
|
for lat := -85.; lat < 90; lat += 10 {
|
|
altitude := basic.HMoonHeight(jd, lon, lat, 0)
|
|
if math.Abs(altitude) < 0.05 {
|
|
continue
|
|
}
|
|
if inside := geometryContainsPoint(t, band.Geometry, lon, lat); inside != (altitude > 0) {
|
|
t.Fatalf("%s point=(%v,%v) inside=%v altitude=%v", contact.role, lon, lat, inside, altitude)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLunarGeoJSON19040924DoesNotFillFalseSouthPolarCap(t *testing.T) {
|
|
date := time.Date(1904, 9, 24, 0, 0, 0, 0, time.UTC)
|
|
info, ok := eclipse.LunarEclipseOnDate(date)
|
|
if !ok {
|
|
t.Fatal("missing lunar eclipse")
|
|
}
|
|
data, err := geojson.MarshalLunarEclipse(info, 360)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
collection := decodeCollection(t, data)
|
|
band := featureWithRole(t, collection, "visible-at-p1")
|
|
point := struct {
|
|
longitude float64
|
|
latitude float64
|
|
}{-115, -89.9}
|
|
if altitude := basic.HMoonHeight(basic.Date2JDE(info.PenumbralStart), point.longitude, point.latitude, 0); altitude >= -0.01 {
|
|
t.Fatalf("regression witness altitude=%g, want below horizon", altitude)
|
|
}
|
|
if geometryContainsPoint(t, band.Geometry, point.longitude, point.latitude) {
|
|
t.Fatalf("visible-at-p1 contains below-horizon polar witness %+v", point)
|
|
}
|
|
}
|
|
|
|
func TestLunarGeoJSONPolarVisibilityGrid(t *testing.T) {
|
|
for _, day := range []string{
|
|
"0275-09-22", "0386-09-24", "1076-09-15", "1904-09-24", "2396-03-25",
|
|
"2779-03-24", "2955-09-23", "3188-09-27", "3738-03-19", "4026-03-16",
|
|
} {
|
|
t.Run(day, func(t *testing.T) {
|
|
date, err := time.Parse("2006-01-02", day)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, ok := eclipse.LunarEclipseOnDate(date)
|
|
if !ok {
|
|
t.Fatal("missing lunar eclipse")
|
|
}
|
|
counts := []int{360}
|
|
if day == "1904-09-24" {
|
|
counts = []int{12, 96, 360, 1440}
|
|
}
|
|
for _, count := range counts {
|
|
t.Run(fmt.Sprint(count), func(t *testing.T) {
|
|
assertLunarVisibilityGrid(t, info, count)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertLunarVisibilityGrid(t *testing.T, info eclipse.LunarEclipseInfo, count int) {
|
|
t.Helper()
|
|
data, err := geojson.MarshalLunarEclipse(info, count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
collection := decodeCollection(t, data)
|
|
latitudes := []float64{-89.999999, -89.999, -89.99, -89.9, -89.5, 89.5, 89.9, 89.99, 89.999, 89.999999}
|
|
for lat := -89.0; lat <= 89; lat += 2 {
|
|
latitudes = append(latitudes, lat)
|
|
}
|
|
for _, contact := range []struct {
|
|
role string
|
|
at time.Time
|
|
}{{"visible-at-p1", info.PenumbralStart}, {"visible-at-p4", info.PenumbralEnd}} {
|
|
band := featureWithRole(t, collection, contact.role)
|
|
var polygons [][][][]float64
|
|
if err := json.Unmarshal(band.Geometry.Coordinates, &polygons); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
jd := basic.Date2JDE(contact.at.UTC())
|
|
visible, invisible := 0, 0
|
|
for _, lat := range latitudes {
|
|
for lon := -179.5; lon < 180; lon += 5 {
|
|
altitude := basic.HMoonHeight(jd, lon, lat, 0)
|
|
tolerance := 0.003
|
|
if math.Abs(lat) > 89.99 {
|
|
tolerance = 1e-5
|
|
}
|
|
if math.Abs(altitude) <= tolerance {
|
|
continue
|
|
}
|
|
inside := geoJSONMultiPolygonContains(polygons, lon, lat)
|
|
if inside != (altitude > 0) {
|
|
t.Fatalf("%s point=(%g,%g) inside=%v altitude=%g", contact.role, lon, lat, inside, altitude)
|
|
}
|
|
if inside {
|
|
visible++
|
|
} else {
|
|
invisible++
|
|
}
|
|
}
|
|
}
|
|
if visible == 0 || invisible == 0 {
|
|
t.Fatalf("%s grid must exercise both sides: visible=%d invisible=%d", contact.role, visible, invisible)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkLunarEclipseGeoJSON(b *testing.B) {
|
|
for _, day := range []string{"1904-09-24", "2026-03-03", "4026-03-16"} {
|
|
date, err := time.Parse("2006-01-02", day)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
info, ok := eclipse.LunarEclipseOnDate(date)
|
|
if !ok {
|
|
b.Fatal("missing lunar eclipse")
|
|
}
|
|
b.Run(day, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for iteration := 0; iteration < b.N; iteration++ {
|
|
if _, err := geojson.MarshalLunarEclipse(info, 360); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|