167 lines
5.5 KiB
Go
167 lines
5.5 KiB
Go
package gpx
|
||
|
||
import (
|
||
"math"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
const trkGPX = `<?xml version="1.0"?>
|
||
<gpx version="1.1" creator="test">
|
||
<metadata><name>元数据名</name></metadata>
|
||
<trk>
|
||
<name>轨迹名</name>
|
||
<trkseg>
|
||
<trkpt lat="30.100000" lon="120.200000"><ele>12.5</ele><time>2026-08-13T07:00:00Z</time></trkpt>
|
||
<trkpt lat="30.101000" lon="120.200000"><ele>13.5</ele><time>2026-08-13T07:00:30Z</time></trkpt>
|
||
</trkseg>
|
||
</trk>
|
||
</gpx>`
|
||
|
||
func TestParseTrk(t *testing.T) {
|
||
ts, err := Parse([]byte(trkGPX))
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if len(ts) != 1 {
|
||
t.Fatalf("应得 1 条轨迹,实际 %d", len(ts))
|
||
}
|
||
tr := ts[0]
|
||
if tr.Name != "轨迹名" {
|
||
t.Errorf("trk 有 name 时用它,实际 %q", tr.Name)
|
||
}
|
||
if len(tr.Points) != 2 {
|
||
t.Fatalf("应得 2 个点,实际 %d", len(tr.Points))
|
||
}
|
||
if tr.Points[0].Ele != 12.5 {
|
||
t.Errorf("trkpt 的高程该带出来,实际 %v", tr.Points[0].Ele)
|
||
}
|
||
want := time.Date(2026, 8, 13, 7, 0, 0, 0, time.UTC)
|
||
if !tr.Points[0].T.Equal(want) {
|
||
t.Errorf("时间解析错: %v", tr.Points[0].T)
|
||
}
|
||
}
|
||
|
||
// trk 没写 name 时退回 metadata>name——否则整条轨迹会没名字。
|
||
func TestParseTrackNameFallsBackToMetadata(t *testing.T) {
|
||
src := strings.Replace(trkGPX, "<name>轨迹名</name>", "", 1)
|
||
ts, err := Parse([]byte(src))
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if ts[0].Name != "元数据名" {
|
||
t.Errorf("应退回 metadata 名字,实际 %q", ts[0].Name)
|
||
}
|
||
}
|
||
|
||
// 没有 trk 时退回 rte/rtept。
|
||
func TestParseFallsBackToRoute(t *testing.T) {
|
||
src := `<?xml version="1.0"?>
|
||
<gpx version="1.1"><metadata><name>路线</name></metadata>
|
||
<rte><name>回家</name>
|
||
<rtept lat="30.100000" lon="120.200000"><ele>9</ele></rtept>
|
||
<rtept lat="30.102000" lon="120.200000"><ele>11</ele></rtept>
|
||
</rte></gpx>`
|
||
ts, err := Parse([]byte(src))
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if len(ts) != 1 || ts[0].Name != "回家" || len(ts[0].Points) != 2 {
|
||
t.Fatalf("route 应解析成一条轨迹: %+v", ts)
|
||
}
|
||
// 与 trkpt 分支不一致:rtept 的高程没被带出来。当前 Point.Ele 全仓无人消费,
|
||
// 所以不影响功能;这条断言是把现状钉住,将来若决定统一,改这里会立刻提醒。
|
||
if ts[0].Points[0].Ele != 0 {
|
||
t.Errorf("rte 分支当前不带高程,实际 %v(如已统一请更新本用例)", ts[0].Points[0].Ele)
|
||
}
|
||
}
|
||
|
||
// (0,0) 是 GPX 里常见的空点,必须丢掉,否则「几内亚湾」会被算进里程。
|
||
func TestParseSkipsNullIsland(t *testing.T) {
|
||
src := `<?xml version="1.0"?>
|
||
<gpx version="1.1"><trk><trkseg>
|
||
<trkpt lat="0" lon="0"/>
|
||
<trkpt lat="30.100000" lon="120.200000"/>
|
||
<trkpt lat="30.101000" lon="120.200000"/>
|
||
</trkseg></trk></gpx>`
|
||
ts, err := Parse([]byte(src))
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if len(ts[0].Points) != 2 {
|
||
t.Fatalf("(0,0) 该被丢掉,实际剩 %d 个点: %+v", len(ts[0].Points), ts[0].Points)
|
||
}
|
||
}
|
||
|
||
// 只有一个点算不出里程,整条丢掉;全丢光就报错,别让调用方以为成功。
|
||
func TestParseDropsSinglePointTrack(t *testing.T) {
|
||
src := `<?xml version="1.0"?>
|
||
<gpx version="1.1"><trk><trkseg>
|
||
<trkpt lat="30.100000" lon="120.200000"/>
|
||
</trkseg></trk></gpx>`
|
||
if ts, err := Parse([]byte(src)); err == nil {
|
||
t.Fatalf("单点轨迹应当报错,实际返回 %+v", ts)
|
||
}
|
||
}
|
||
|
||
func TestParseErrors(t *testing.T) {
|
||
if _, err := Parse([]byte("这不是 XML")); err == nil ||
|
||
!strings.Contains(err.Error(), "GPX 解析失败") {
|
||
t.Errorf("非法 XML 要报「GPX 解析失败」,实际 %v", err)
|
||
}
|
||
empty := `<?xml version="1.0"?><gpx version="1.1"><metadata><name>x</name></metadata></gpx>`
|
||
_, err := Parse([]byte(empty))
|
||
if err == nil || !strings.Contains(err.Error(), "没有找到轨迹点") {
|
||
t.Errorf("没有轨迹点要明确报出来,实际 %v", err)
|
||
}
|
||
}
|
||
|
||
// 时间格式不对时不该整个失败,只是那个点没有时间。
|
||
func TestParseToleratesBadTime(t *testing.T) {
|
||
src := `<?xml version="1.0"?>
|
||
<gpx version="1.1"><trk><trkseg>
|
||
<trkpt lat="30.100000" lon="120.200000"><time>2026/08/13 07:00</time></trkpt>
|
||
<trkpt lat="30.101000" lon="120.200000"/>
|
||
</trkseg></trk></gpx>`
|
||
ts, err := Parse([]byte(src))
|
||
if err != nil {
|
||
t.Fatalf("解析失败: %v", err)
|
||
}
|
||
if !ts[0].Time.IsZero() {
|
||
t.Errorf("时间解析不出来时应为零值,实际 %v", ts[0].Time)
|
||
}
|
||
}
|
||
|
||
// 对跖点附近 h 会因浮点误差略大于 1(实测最多到 1+2^-52)。geo.Haversine 用
|
||
// math.Min(1, …) 夹了一道,gpx 这份没有——但那个夹子在这里其实够不着:sqrt(1+2^-52)
|
||
// 会回舍成正好 1.0,asin(1.0)=π/2,出不了 NaN。这条用例把「对跖点不出 NaN」钉住,
|
||
// 将来若有人换公式或去掉开方,会立刻红。
|
||
func TestLengthKmAntipodalIsFinite(t *testing.T) {
|
||
for _, c := range [][4]float64{
|
||
{45, 0, -45, 180},
|
||
{0, 0, 0, 180},
|
||
{30.5, 10.25, -30.5, -169.75},
|
||
} {
|
||
tr := Track{Points: []Point{{Lat: c[0], Lon: c[1]}, {Lat: c[2], Lon: c[3]}}}
|
||
if got := tr.LengthKm(); math.IsNaN(got) {
|
||
t.Errorf("对跖点 %v 算出了 NaN", c)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLengthKm(t *testing.T) {
|
||
// 纬度差 1 度 ≈ 6371*π/180 = 111.19 km
|
||
tr := Track{Points: []Point{{Lat: 30, Lon: 120}, {Lat: 31, Lon: 120}}}
|
||
got := tr.LengthKm()
|
||
if math.Abs(got-111.195) > 0.01 {
|
||
t.Errorf("1 度纬度该约 111.19 km,实际 %.4f", got)
|
||
}
|
||
if n := (Track{Points: []Point{{Lat: 30, Lon: 120}}}).LengthKm(); n != 0 {
|
||
t.Errorf("单点里程该为 0,实际 %v", n)
|
||
}
|
||
if n := (Track{}).LengthKm(); n != 0 {
|
||
t.Errorf("空轨迹里程该为 0,实际 %v", n)
|
||
}
|
||
}
|