first release
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user