2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
87 lines
3.2 KiB
Go
87 lines
3.2 KiB
Go
package svgmap
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"b612.me/astro/internal/geodata"
|
|
)
|
|
|
|
var landGlobeCoordinatePattern = regexp.MustCompile(`[ML](-?[0-9.]+) (-?[0-9.]+)`)
|
|
|
|
// renderGlobeLand 渲染球面底图并取回陆地的路径命令与画布。
|
|
func renderGlobeLand(t *testing.T, center geodata.GeoPoint) (Frame, string) {
|
|
t.Helper()
|
|
frame := Frame{
|
|
X: 30, Y: 30, Width: 640, Height: 640, Projection: ProjectionOrthographic,
|
|
CenterLongitude: center.Longitude, CenterLatitude: center.Latitude,
|
|
}
|
|
var builder strings.Builder
|
|
fmt.Fprintf(&builder, `<svg xmlns="http://www.w3.org/2000/svg" width="700" height="700"><defs>%s</defs>`,
|
|
frame.ClipDefinition("clip"))
|
|
frame.WriteOcean(&builder)
|
|
frame.WriteGraticule(&builder, "clip")
|
|
frame.WriteLand(&builder, "clip")
|
|
builder.WriteString(`</svg>`)
|
|
rendered := builder.String()
|
|
const marker = `<path class="land" d="`
|
|
rest := rendered[strings.Index(rendered, marker)+len(marker):]
|
|
return frame, rest[:strings.Index(rest, `"`)]
|
|
}
|
|
|
|
// 球面底图的输出路径必须只由短边组成,且全部落在圆盘内。
|
|
// 这条断言针对的是一个真实缺陷:把已经裁好的环再交给 PolylineSegments 二次裁剪时,
|
|
// 视界上的点深度恰为 0 会被判成不可见,环被拆成多段,每段再用 Z 直线闭合,
|
|
// 于是圆盘上出现一条横穿的弦——亚洲大陆看起来就"少了一块"。
|
|
func TestOrthographicLandPathHasNoChords(t *testing.T) {
|
|
for _, center := range []geodata.GeoPoint{
|
|
{Longitude: 144.1, Latitude: 24.2},
|
|
{Longitude: -35.9, Latitude: -24.2},
|
|
{Longitude: 0, Latitude: 80},
|
|
} {
|
|
frame, path := renderGlobeLand(t, center)
|
|
subpaths := make([][][2]float64, 0, 2048)
|
|
for _, match := range landGlobeCoordinatePattern.FindAllStringSubmatch(path, -1) {
|
|
x, errX := strconv.ParseFloat(match[1], 64)
|
|
y, errY := strconv.ParseFloat(match[2], 64)
|
|
if errX != nil || errY != nil {
|
|
t.Fatalf("unparsable coordinate %q %q", match[1], match[2])
|
|
}
|
|
if len(subpaths) == 0 || match[0][0] == 'M' {
|
|
subpaths = append(subpaths, nil)
|
|
}
|
|
subpaths[len(subpaths)-1] = append(subpaths[len(subpaths)-1], [2]float64{x, y})
|
|
}
|
|
if len(subpaths) < 100 {
|
|
t.Fatalf("%v: only %d subpaths rendered", center, len(subpaths))
|
|
}
|
|
centerX := frame.X + frame.Width/2
|
|
centerY := frame.Y + frame.Height/2
|
|
radius := math.Min(frame.Width, frame.Height) / 2
|
|
// 视界闭合弧按 180 段加密,整圈弦长约 2°;留 4 倍余量即可判定"这条边不可能是海岸线"。
|
|
limit := radius * 0.09
|
|
vertices := 0
|
|
for _, subpath := range subpaths {
|
|
if len(subpath) < 3 {
|
|
t.Fatalf("%v: subpath with %d points", center, len(subpath))
|
|
}
|
|
vertices += len(subpath)
|
|
for index, point := range subpath {
|
|
if math.Hypot(point[0]-centerX, point[1]-centerY) > radius+0.6 {
|
|
t.Fatalf("%v: vertex %.1f,%.1f falls outside the disk", center, point[0], point[1])
|
|
}
|
|
next := subpath[(index+1)%len(subpath)]
|
|
if gap := math.Hypot(next[0]-point[0], next[1]-point[1]); gap > limit {
|
|
t.Fatalf("%v: subpath edge of %.1f px (limit %.1f) between %.1f,%.1f and %.1f,%.1f",
|
|
center, gap, limit, point[0], point[1], next[0], next[1])
|
|
}
|
|
}
|
|
}
|
|
t.Logf("%v: subpaths=%d vertices=%d", center, len(subpaths), vertices)
|
|
}
|
|
}
|