feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package geojson_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro"
|
||||
"b612.me/astro/geojson"
|
||||
"b612.me/astro/mercury"
|
||||
"b612.me/astro/venus"
|
||||
)
|
||||
|
||||
type transitCollection struct {
|
||||
TimeScale string `json:"time_scale"`
|
||||
Features []struct {
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
Geometry struct {
|
||||
Type string `json:"type"`
|
||||
Coordinates json.RawMessage `json:"coordinates"`
|
||||
} `json:"geometry"`
|
||||
} `json:"features"`
|
||||
}
|
||||
|
||||
func decodeTransit(t *testing.T, data []byte) (transitCollection, map[string]int) {
|
||||
t.Helper()
|
||||
var collection transitCollection
|
||||
if err := json.Unmarshal(data, &collection); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roles := map[string]int{}
|
||||
for _, item := range collection.Features {
|
||||
if item.Properties["event"] != "planet-transit" {
|
||||
t.Fatalf("unexpected event %v", item.Properties["event"])
|
||||
}
|
||||
roles[item.Properties["role"].(string)]++
|
||||
}
|
||||
return collection, roles
|
||||
}
|
||||
|
||||
// 2032-11-13 水星凌日:等时线四个接触俱全,区域与地平线成对输出,凌甚点落在太阳直射点。
|
||||
func TestMarshalMercuryTransit(t *testing.T) {
|
||||
info := mercury.ClosestTransit(time.Date(2032, 11, 13, 0, 0, 0, 0, time.UTC))
|
||||
data, err := geojson.MarshalMercuryTransit(info, geojson.PlanetTransitOptions{
|
||||
IsochroneStep: 20 * time.Second,
|
||||
TimeMarkers: &geojson.TimeMarkerOptions{Step: 30 * time.Minute},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
collection, roles := decodeTransit(t, data)
|
||||
for _, role := range []string{
|
||||
"visible-at-c1", "c1-horizon", "visible-at-c4", "c4-horizon",
|
||||
"transit-entire", "transit-at-sunrise", "transit-at-sunset",
|
||||
"c1-isochrone", "c2-isochrone", "c3-isochrone", "c4-isochrone", "greatest",
|
||||
} {
|
||||
if roles[role] != 1 {
|
||||
t.Fatalf("role %s count = %d, want 1", role, roles[role])
|
||||
}
|
||||
}
|
||||
if roles["time-marker"] < 8 {
|
||||
t.Fatalf("time markers = %d, want one every 30 min over ~4.4 h", roles["time-marker"])
|
||||
}
|
||||
if collection.TimeScale != "" {
|
||||
t.Fatal("UTC export must not carry time_scale")
|
||||
}
|
||||
for _, item := range collection.Features {
|
||||
role := item.Properties["role"]
|
||||
switch role {
|
||||
case "c1-isochrone", "c2-isochrone", "c3-isochrone", "c4-isochrone":
|
||||
var lines [][][]float64
|
||||
if err := json.Unmarshal(item.Geometry.Coordinates, &lines); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
times := item.Properties["times"].([]interface{})
|
||||
if len(times) != len(lines) || len(lines) == 0 {
|
||||
t.Fatalf("%s: times %d do not align with %d lines", role, len(times), len(lines))
|
||||
}
|
||||
for index, line := range lines {
|
||||
if len(times[index].([]interface{})) != len(line) {
|
||||
t.Fatalf("%s line %d: times length mismatch", role, index)
|
||||
}
|
||||
}
|
||||
case "greatest":
|
||||
if item.Properties["internal_start"] == nil || item.Properties["minimum_separation_arcsec"] == nil {
|
||||
t.Fatal("greatest point must carry contact metadata")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认不出等时线;SkipRoles 跳过全部分区后只剩地平线、可见区与凌甚点。
|
||||
func TestMarshalVenusTransitDefaultsAndSkip(t *testing.T) {
|
||||
info := venus.ClosestTransit(time.Date(2012, 6, 6, 0, 0, 0, 0, time.UTC))
|
||||
data, err := geojson.MarshalVenusTransit(info, geojson.PlanetTransitOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, roles := decodeTransit(t, data)
|
||||
if roles["c1-isochrone"] != 0 || roles["transit-entire"] != 1 {
|
||||
t.Fatalf("default roles = %v", roles)
|
||||
}
|
||||
data, err = geojson.MarshalVenusTransit(info, geojson.PlanetTransitOptions{
|
||||
SkipRoles: []string{"transit-entire", "transit-at-sunrise", "transit-at-sunset"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, roles = decodeTransit(t, data)
|
||||
if len(roles) != 5 {
|
||||
t.Fatalf("skipped region roles = %v", roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalPlanetTransitValidation(t *testing.T) {
|
||||
info := mercury.ClosestTransit(time.Date(2019, 11, 11, 0, 0, 0, 0, time.UTC))
|
||||
if _, err := geojson.MarshalMercuryTransit(mercury.TransitInfo{}, geojson.PlanetTransitOptions{}); err == nil {
|
||||
t.Fatal("invalid transit must be rejected")
|
||||
}
|
||||
if _, err := geojson.MarshalMercuryTransit(info, geojson.PlanetTransitOptions{IsochroneStep: time.Millisecond}); err == nil {
|
||||
t.Fatal("sub-second isochrone step must be rejected")
|
||||
}
|
||||
swapped := info
|
||||
swapped.Start, swapped.End = info.End, info.Start
|
||||
if _, err := geojson.MarshalMercuryTransit(swapped, geojson.PlanetTransitOptions{}); err == nil {
|
||||
t.Fatal("unordered contacts must be rejected")
|
||||
}
|
||||
inconsistent := info
|
||||
inconsistent.HasInternal = false
|
||||
if _, err := geojson.MarshalMercuryTransit(inconsistent, geojson.PlanetTransitOptions{IsochroneStep: time.Minute}); err == nil {
|
||||
t.Fatal("internal contact times without HasInternal must be rejected")
|
||||
}
|
||||
if _, err := geojson.MarshalMercuryTransit(info, geojson.PlanetTransitOptions{
|
||||
TimeMarkers: &geojson.TimeMarkerOptions{TimeScale: astro.TimeScaleUT1, Location: time.FixedZone("CST", 8*3600)},
|
||||
}); err == nil {
|
||||
t.Fatal("UT1 with a non-UTC location must be rejected")
|
||||
}
|
||||
data, err := geojson.MarshalMercuryTransit(info, geojson.PlanetTransitOptions{
|
||||
TimeMarkers: &geojson.TimeMarkerOptions{TimeScale: astro.TimeScaleUT1},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if collection, _ := decodeTransit(t, data); collection.TimeScale != "UT1" {
|
||||
t.Fatal("UT1 export must carry time_scale")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user