158 lines
3.3 KiB
Go
158 lines
3.3 KiB
Go
// Package gpx 解析 GPX 轨迹文件(标准 WGS-84 坐标),
|
||
// 用来给 rond 的「地点到地点」直线位移补上真实路径。
|
||
package gpx
|
||
|
||
import (
|
||
"encoding/xml"
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Track 是一条轨迹(GPX 的 trk / rte),坐标是 WGS-84。
|
||
type Track struct {
|
||
Name string
|
||
Points []Point
|
||
Time time.Time
|
||
}
|
||
|
||
type Point struct {
|
||
Lat float64
|
||
Lon float64
|
||
Ele float64
|
||
T time.Time
|
||
}
|
||
|
||
type gpxDoc struct {
|
||
XMLName xml.Name `xml:"gpx"`
|
||
Name string `xml:"metadata>name"`
|
||
Trks []trk `xml:"trk"`
|
||
Rtes []rte `xml:"rte"`
|
||
}
|
||
|
||
type trk struct {
|
||
Name string `xml:"name"`
|
||
Trkseg []trkseg `xml:"trkseg"`
|
||
}
|
||
|
||
type trkseg struct {
|
||
Pts []wpt `xml:"trkpt"`
|
||
}
|
||
|
||
type rte struct {
|
||
Name string `xml:"name"`
|
||
Pts []wpt `xml:"rtept"`
|
||
}
|
||
|
||
type wpt struct {
|
||
Lat float64 `xml:"lat,attr"`
|
||
Lon float64 `xml:"lon,attr"`
|
||
Ele float64 `xml:"ele"`
|
||
T string `xml:"time"`
|
||
}
|
||
|
||
// Parse 解析 GPX 文本,返回其中的全部轨迹。trk 优先,没有则退回 rte。
|
||
func Parse(data []byte) ([]Track, error) {
|
||
var doc gpxDoc
|
||
if err := xml.Unmarshal(data, &doc); err != nil {
|
||
return nil, fmt.Errorf("GPX 解析失败: %w", err)
|
||
}
|
||
var out []Track
|
||
base := strings.TrimSpace(doc.Name)
|
||
for _, t := range doc.Trks {
|
||
tr := Track{Name: firstNonEmpty(strings.TrimSpace(t.Name), base)}
|
||
for _, seg := range t.Trkseg {
|
||
for _, p := range seg.Pts {
|
||
if p.Lat == 0 && p.Lon == 0 {
|
||
continue
|
||
}
|
||
pt := Point{Lat: p.Lat, Lon: p.Lon, Ele: p.Ele}
|
||
if p.T != "" {
|
||
pt.T, _ = time.Parse(time.RFC3339, strings.TrimSpace(p.T))
|
||
}
|
||
tr.Points = append(tr.Points, pt)
|
||
}
|
||
}
|
||
if len(tr.Points) > 1 {
|
||
if tr.Time.IsZero() {
|
||
tr.Time = tr.Points[0].T
|
||
}
|
||
out = append(out, tr)
|
||
}
|
||
}
|
||
for _, r := range doc.Rtes {
|
||
tr := Track{Name: firstNonEmpty(strings.TrimSpace(r.Name), base)}
|
||
for _, p := range r.Pts {
|
||
if p.Lat == 0 && p.Lon == 0 {
|
||
continue
|
||
}
|
||
pt := Point{Lat: p.Lat, Lon: p.Lon}
|
||
if p.T != "" {
|
||
pt.T, _ = time.Parse(time.RFC3339, strings.TrimSpace(p.T))
|
||
}
|
||
tr.Points = append(tr.Points, pt)
|
||
}
|
||
if len(tr.Points) > 1 {
|
||
if tr.Time.IsZero() {
|
||
tr.Time = tr.Points[0].T
|
||
}
|
||
out = append(out, tr)
|
||
}
|
||
}
|
||
if len(out) == 0 {
|
||
return nil, errors.New("文件里没有找到轨迹点(需要 trk/trkpt 或 rte/rtept)")
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func firstNonEmpty(vals ...string) string {
|
||
for _, v := range vals {
|
||
if v != "" {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// LengthKm 计算轨迹里程(Haversine)。
|
||
func (t Track) LengthKm() float64 {
|
||
sum := 0.0
|
||
for i := 1; i < len(t.Points); i++ {
|
||
sum += haversine(t.Points[i-1], t.Points[i])
|
||
}
|
||
return sum
|
||
}
|
||
|
||
// BBox 返回 [minLon, minLat, maxLon, maxLat]。
|
||
func (t Track) BBox() [4]float64 {
|
||
b := [4]float64{180, 90, -180, -90}
|
||
for _, p := range t.Points {
|
||
if p.Lon < b[0] {
|
||
b[0] = p.Lon
|
||
}
|
||
if p.Lat < b[1] {
|
||
b[1] = p.Lat
|
||
}
|
||
if p.Lon > b[2] {
|
||
b[2] = p.Lon
|
||
}
|
||
if p.Lat > b[3] {
|
||
b[3] = p.Lat
|
||
}
|
||
}
|
||
return b
|
||
}
|
||
|
||
func haversine(a, b Point) float64 {
|
||
const r = 6371.0
|
||
dLat := (b.Lat - a.Lat) * math.Pi / 180
|
||
dLon := (b.Lon - a.Lon) * math.Pi / 180
|
||
la1 := a.Lat * math.Pi / 180
|
||
la2 := b.Lat * math.Pi / 180
|
||
h := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||
math.Cos(la1)*math.Cos(la2)*math.Sin(dLon/2)*math.Sin(dLon/2)
|
||
return 2 * r * math.Asin(math.Sqrt(h))
|
||
}
|