mirror of
https://github.com/Luzifer/staticmap.git
synced 2024-12-21 05:11:18 +00:00
1582 lines
40 KiB
Go
1582 lines
40 KiB
Go
// Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
|
|
// rights reserved. Use of this source code is governed
|
|
// by a BSD-style license that can be found in the
|
|
// LICENSE file.
|
|
|
|
package gpx
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DEFAULT_STOPPED_SPEED_THRESHOLD = 1.0
|
|
REMOVE_EXTREEMES_TRESHOLD = 10
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
// Some basic stats all common GPX elements (GPX, track and segment) must have
|
|
type GPXElementInfo interface {
|
|
Length2D() float64
|
|
Length3D() float64
|
|
Bounds() GpxBounds
|
|
MovingData() MovingData
|
|
UphillDownhill() UphillDownhill
|
|
TimeBounds() TimeBounds
|
|
GetTrackPointsNo() int
|
|
}
|
|
|
|
// Pretty prints some basic information about this GPX elements
|
|
func GetGpxElementInfo(prefix string, gpxDoc GPXElementInfo) string {
|
|
result := ""
|
|
result += fmt.Sprint(prefix, " Points: ", gpxDoc.GetTrackPointsNo(), "\n")
|
|
result += fmt.Sprint(prefix, " Length 2D: ", gpxDoc.Length2D()/1000.0, "\n")
|
|
result += fmt.Sprint(prefix, " Length 3D: ", gpxDoc.Length3D()/1000.0, "\n")
|
|
|
|
bounds := gpxDoc.Bounds()
|
|
result += fmt.Sprintf("%s Bounds: %f, %f, %f, %f\n", prefix, bounds.MinLatitude, bounds.MaxLatitude, bounds.MinLongitude, bounds.MaxLongitude)
|
|
|
|
md := gpxDoc.MovingData()
|
|
result += fmt.Sprint(prefix, " Moving time: ", md.MovingTime, "\n")
|
|
result += fmt.Sprint(prefix, " Stopped time: ", md.StoppedTime, "\n")
|
|
|
|
result += fmt.Sprintf("%s Max speed: %fm/s = %fkm/h\n", prefix, md.MaxSpeed, md.MaxSpeed*60*60/1000.0)
|
|
|
|
updo := gpxDoc.UphillDownhill()
|
|
result += fmt.Sprint(prefix, " Total uphill: ", updo.Uphill, "\n")
|
|
result += fmt.Sprint(prefix, " Total downhill: ", updo.Downhill, "\n")
|
|
|
|
timeBounds := gpxDoc.TimeBounds()
|
|
result += fmt.Sprint(prefix, " Started: ", timeBounds.StartTime, "\n")
|
|
result += fmt.Sprint(prefix, " Ended: ", timeBounds.EndTime, "\n")
|
|
return result
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type GPX struct {
|
|
XMLNs string
|
|
XmlNsXsi string
|
|
XmlSchemaLoc string
|
|
|
|
Version string
|
|
Creator string
|
|
Name string
|
|
Description string
|
|
AuthorName string
|
|
AuthorEmail string
|
|
AuthorLink string
|
|
AuthorLinkText string
|
|
AuthorLinkType string
|
|
Copyright string
|
|
CopyrightYear string
|
|
CopyrightLicense string
|
|
Link string
|
|
LinkText string
|
|
LinkType string
|
|
Time *time.Time
|
|
Keywords string
|
|
|
|
// TODO
|
|
//Extensions []byte
|
|
Waypoints []GPXPoint
|
|
Routes []GPXRoute
|
|
Tracks []GPXTrack
|
|
}
|
|
|
|
// Params are optional, you can set null to use GPXs Version and no indentation.
|
|
func (g *GPX) ToXml(params ToXmlParams) ([]byte, error) {
|
|
return ToXml(g, params)
|
|
}
|
|
|
|
// Pretty prints some basic information about this GPX, its track and segments
|
|
func (g *GPX) GetGpxInfo() string {
|
|
result := ""
|
|
result += fmt.Sprint("GPX name: ", g.Name, "\n")
|
|
result += fmt.Sprint("GPX desctiption: ", g.Description, "\n")
|
|
result += fmt.Sprint("GPX version: ", g.Version, "\n")
|
|
result += fmt.Sprint("Author: ", g.AuthorName, "\n")
|
|
result += fmt.Sprint("Email: ", g.AuthorEmail, "\n\n")
|
|
|
|
result += fmt.Sprint("\nGlobal stats:", "\n")
|
|
result += GetGpxElementInfo("", g)
|
|
result += "\n"
|
|
|
|
for trackNo, track := range g.Tracks {
|
|
result += fmt.Sprintf("\nTrack #%d:\n", 1+trackNo)
|
|
result += GetGpxElementInfo(" ", &track)
|
|
result += "\n"
|
|
for segmentNo, segment := range track.Segments {
|
|
result += fmt.Sprintf("\nTrack #%d, segment #%d:\n", 1+trackNo, 1+segmentNo)
|
|
result += GetGpxElementInfo(" ", &segment)
|
|
result += "\n"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (g *GPX) GetTrackPointsNo() int {
|
|
result := 0
|
|
for _, track := range g.Tracks {
|
|
result += track.GetTrackPointsNo()
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Length2D returns the 2D length of all tracks in a Gpx.
|
|
func (g *GPX) Length2D() float64 {
|
|
var length2d float64
|
|
for _, trk := range g.Tracks {
|
|
length2d += trk.Length2D()
|
|
}
|
|
return length2d
|
|
}
|
|
|
|
// Length3D returns the 3D length of all tracks,
|
|
func (g *GPX) Length3D() float64 {
|
|
var length3d float64
|
|
for _, trk := range g.Tracks {
|
|
length3d += trk.Length3D()
|
|
}
|
|
return length3d
|
|
}
|
|
|
|
// TimeBounds returns the time bounds of all tacks in a Gpx.
|
|
func (g *GPX) TimeBounds() TimeBounds {
|
|
var tbGpx TimeBounds
|
|
for i, trk := range g.Tracks {
|
|
tbTrk := trk.TimeBounds()
|
|
if i == 0 {
|
|
tbGpx = trk.TimeBounds()
|
|
} else {
|
|
tbGpx.EndTime = tbTrk.EndTime
|
|
}
|
|
}
|
|
return tbGpx
|
|
}
|
|
|
|
// Bounds returns the bounds of all tracks in a Gpx.
|
|
func (g *GPX) Bounds() GpxBounds {
|
|
minmax := getMaximalGpxBounds()
|
|
for _, trk := range g.Tracks {
|
|
bnds := trk.Bounds()
|
|
minmax.MaxLatitude = math.Max(bnds.MaxLatitude, minmax.MaxLatitude)
|
|
minmax.MinLatitude = math.Min(bnds.MinLatitude, minmax.MinLatitude)
|
|
minmax.MaxLongitude = math.Max(bnds.MaxLongitude, minmax.MaxLongitude)
|
|
minmax.MinLongitude = math.Min(bnds.MinLongitude, minmax.MinLongitude)
|
|
}
|
|
return minmax
|
|
}
|
|
|
|
func (g *GPX) ElevationBounds() ElevationBounds {
|
|
minmax := getMaximalElevationBounds()
|
|
for _, trk := range g.Tracks {
|
|
bnds := trk.ElevationBounds()
|
|
minmax.MaxElevation = math.Max(bnds.MaxElevation, minmax.MaxElevation)
|
|
minmax.MinElevation = math.Min(bnds.MinElevation, minmax.MinElevation)
|
|
}
|
|
return minmax
|
|
}
|
|
|
|
// MovingData returns the moving data for all tracks in a Gpx.
|
|
func (g *GPX) MovingData() MovingData {
|
|
var (
|
|
movingTime float64
|
|
stoppedTime float64
|
|
movingDistance float64
|
|
stoppedDistance float64
|
|
maxSpeed float64
|
|
)
|
|
|
|
for _, trk := range g.Tracks {
|
|
md := trk.MovingData()
|
|
movingTime += md.MovingTime
|
|
stoppedTime += md.StoppedTime
|
|
movingDistance += md.MovingDistance
|
|
stoppedDistance += md.StoppedDistance
|
|
|
|
if md.MaxSpeed > maxSpeed {
|
|
maxSpeed = md.MaxSpeed
|
|
}
|
|
}
|
|
return MovingData{
|
|
MovingTime: movingTime,
|
|
MovingDistance: movingDistance,
|
|
StoppedTime: stoppedTime,
|
|
StoppedDistance: stoppedDistance,
|
|
MaxSpeed: maxSpeed,
|
|
}
|
|
}
|
|
|
|
func (g *GPX) ReduceTrackPoints(maxPointsNo int, minDistanceBetween float64) {
|
|
pointsNo := g.GetTrackPointsNo()
|
|
|
|
if pointsNo < maxPointsNo && minDistanceBetween <= 0 {
|
|
return
|
|
}
|
|
|
|
length := g.Length3D()
|
|
|
|
minDistance := math.Max(float64(minDistanceBetween), math.Ceil(length/float64(maxPointsNo)))
|
|
|
|
for _, track := range g.Tracks {
|
|
track.ReduceTrackPoints(minDistance)
|
|
}
|
|
}
|
|
|
|
func (g *GPX) SimplifyTracks(maxDistance float64) {
|
|
for _, track := range g.Tracks {
|
|
track.SimplifyTracks(maxDistance)
|
|
}
|
|
}
|
|
|
|
// Split splits the Gpx segment segNo in a given track trackNo at
|
|
// pointNo.
|
|
func (g *GPX) Split(trackNo, segNo, pointNo int) {
|
|
if trackNo >= len(g.Tracks) {
|
|
return
|
|
}
|
|
|
|
track := &g.Tracks[trackNo]
|
|
|
|
track.Split(segNo, pointNo)
|
|
}
|
|
|
|
// Duration returns the duration of all tracks in a Gpx in seconds.
|
|
func (g *GPX) Duration() float64 {
|
|
if len(g.Tracks) == 0 {
|
|
return 0.0
|
|
}
|
|
var result float64
|
|
for _, trk := range g.Tracks {
|
|
result += trk.Duration()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// UphillDownhill returns uphill and downhill values for all tracks in a
|
|
// Gpx.
|
|
func (g *GPX) UphillDownhill() UphillDownhill {
|
|
if len(g.Tracks) == 0 {
|
|
return UphillDownhill{
|
|
Uphill: 0.0,
|
|
Downhill: 0.0,
|
|
}
|
|
}
|
|
|
|
var (
|
|
uphill float64
|
|
downhill float64
|
|
)
|
|
|
|
for _, trk := range g.Tracks {
|
|
updo := trk.UphillDownhill()
|
|
|
|
uphill += updo.Uphill
|
|
downhill += updo.Downhill
|
|
}
|
|
|
|
return UphillDownhill{
|
|
Uphill: uphill,
|
|
Downhill: downhill,
|
|
}
|
|
}
|
|
|
|
// Checks if *tracks* and segments have time information. Routes and Waypoints are ignored.
|
|
func (g *GPX) HasTimes() bool {
|
|
result := true
|
|
for _, track := range g.Tracks {
|
|
result = result && track.HasTimes()
|
|
}
|
|
return result
|
|
}
|
|
|
|
// PositionAt returns a LocationResultsPair consisting the segment index
|
|
// and the GpxWpt at a certain time.
|
|
func (g *GPX) PositionAt(t time.Time) []TrackPosition {
|
|
results := make([]TrackPosition, 0)
|
|
|
|
for trackNo, trk := range g.Tracks {
|
|
locs := trk.PositionAt(t)
|
|
if len(locs) > 0 {
|
|
for locNo := range locs {
|
|
locs[locNo].TrackNo = trackNo
|
|
}
|
|
results = append(results, locs...)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
func (g *GPX) StoppedPositions() []TrackPosition {
|
|
result := make([]TrackPosition, 0)
|
|
for trackNo, track := range g.Tracks {
|
|
positions := track.StoppedPositions()
|
|
for _, position := range positions {
|
|
position.TrackNo = trackNo
|
|
result = append(result, position)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (g *GPX) getDistancesFromStart(distanceBetweenPoints float64) [][][]float64 {
|
|
result := make([][][]float64, len(g.Tracks))
|
|
var fromStart float64
|
|
var lastSampledPoint float64
|
|
for trackNo, track := range g.Tracks {
|
|
result[trackNo] = make([][]float64, len(track.Segments))
|
|
for segmentNo, segment := range track.Segments {
|
|
result[trackNo][segmentNo] = make([]float64, len(segment.Points))
|
|
for pointNo, point := range segment.Points {
|
|
if pointNo > 0 {
|
|
fromStart += point.Distance2D(&segment.Points[pointNo-1])
|
|
}
|
|
if pointNo == 0 || pointNo == len(segment.Points)-1 || fromStart-lastSampledPoint > distanceBetweenPoints {
|
|
result[trackNo][segmentNo][pointNo] = fromStart
|
|
lastSampledPoint = fromStart
|
|
} else {
|
|
result[trackNo][segmentNo][pointNo] = -1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Finds locations candidates where this location is on a track. Returns an
|
|
// array of distances from start for every given location. Used (for example)
|
|
// for positioning waypoints on the graph.
|
|
// The bigger the samples number the more granular the search will be. For
|
|
// example if samples is 100 then (cca) every 100th point will be searched.
|
|
// This is for tracks with thousands of waypoints -- computing distances for
|
|
// each and every point is slow.
|
|
func (g *GPX) GetLocationsPositionsOnTrack(samples int, locations ...Location) [][]float64 {
|
|
length2d := g.Length2D()
|
|
distancesFromStart := g.getDistancesFromStart(length2d / float64(samples))
|
|
result := make([][]float64, len(locations))
|
|
for locationNo, location := range locations {
|
|
result[locationNo] = g.getPositionsOnTrackWithPrecomputedDistances(location, distancesFromStart, length2d)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Use always GetLocationsPositionsOnTrack(...) for multiple points, it is
|
|
// faster.
|
|
func (g *GPX) GetLocationPositionsOnTrack(samples int, location Location) []float64 {
|
|
return g.GetLocationsPositionsOnTrack(samples, location)[0]
|
|
}
|
|
|
|
// distancesFromStart must have the same tracks, segments and pointsNo as this track.
|
|
// if any distance in distancesFromStart is less than zero that point is ignored.
|
|
func (g *GPX) getPositionsOnTrackWithPrecomputedDistances(location Location, distancesFromStart [][][]float64, length2d float64) []float64 {
|
|
if len(g.Tracks) == 0 {
|
|
return []float64{}
|
|
}
|
|
|
|
// The point must be closer than this value in order to be a candidate location:
|
|
minDistance := 0.01 * length2d
|
|
pointLocations := make([]float64, 0)
|
|
|
|
// True when we enter under the minDistance length
|
|
nearerThanMinDistance := false
|
|
|
|
var currentCandidate *GPXPoint
|
|
var currentCandidateFromStart float64
|
|
currentCandidateDistance := minDistance
|
|
|
|
var fromStart float64
|
|
for trackNo, track := range g.Tracks {
|
|
for segmentNo, segment := range track.Segments {
|
|
for pointNo, point := range segment.Points {
|
|
fromStart = distancesFromStart[trackNo][segmentNo][pointNo]
|
|
if fromStart >= 0 {
|
|
distance := point.Distance2D(location)
|
|
nearerThanMinDistance = distance < minDistance
|
|
if nearerThanMinDistance {
|
|
if distance < currentCandidateDistance {
|
|
currentCandidate = &point
|
|
currentCandidateDistance = distance
|
|
currentCandidateFromStart = fromStart
|
|
}
|
|
} else {
|
|
if currentCandidate != nil {
|
|
pointLocations = append(pointLocations, currentCandidateFromStart)
|
|
}
|
|
currentCandidate = nil
|
|
currentCandidateDistance = minDistance
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if currentCandidate != nil {
|
|
pointLocations = append(pointLocations, currentCandidateFromStart)
|
|
}
|
|
|
|
return pointLocations
|
|
}
|
|
|
|
func (g *GPX) ExecuteOnAllPoints(executor func(*GPXPoint)) {
|
|
g.ExecuteOnWaypoints(executor)
|
|
g.ExecuteOnRoutePoints(executor)
|
|
g.ExecuteOnTrackPoints(executor)
|
|
}
|
|
|
|
func (g *GPX) ExecuteOnWaypoints(executor func(*GPXPoint)) {
|
|
for waypointNo := range g.Waypoints {
|
|
executor(&g.Waypoints[waypointNo])
|
|
}
|
|
}
|
|
|
|
func (g *GPX) ExecuteOnRoutePoints(executor func(*GPXPoint)) {
|
|
for _, route := range g.Routes {
|
|
route.ExecuteOnPoints(executor)
|
|
}
|
|
}
|
|
|
|
func (g *GPX) ExecuteOnTrackPoints(executor func(*GPXPoint)) {
|
|
for _, track := range g.Tracks {
|
|
track.ExecuteOnPoints(executor)
|
|
}
|
|
}
|
|
|
|
func (g *GPX) AddElevation(elevation float64) {
|
|
g.ExecuteOnAllPoints(func(point *GPXPoint) {
|
|
fmt.Println("setting elevation if NotNull for:", point.Elevation)
|
|
if point.Elevation.NotNull() {
|
|
fmt.Println("setting elevation")
|
|
point.Elevation.SetValue(point.Elevation.Value() + elevation)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (g *GPX) RemoveElevation() {
|
|
g.ExecuteOnAllPoints(func(point *GPXPoint) {
|
|
point.Elevation.SetNull()
|
|
})
|
|
}
|
|
|
|
func (g *GPX) ReduceGpxToSingleTrack() {
|
|
if len(g.Tracks) <= 1 {
|
|
return
|
|
}
|
|
|
|
firstTrack := &g.Tracks[0]
|
|
for _, track := range g.Tracks[1:] {
|
|
for _, segment := range track.Segments {
|
|
firstTrack.AppendSegment(&segment)
|
|
}
|
|
}
|
|
|
|
g.Tracks = []GPXTrack{*firstTrack}
|
|
}
|
|
|
|
// Removes all a) segments without points and b) tracks without segments
|
|
func (g *GPX) RemoveEmpty() {
|
|
if len(g.Tracks) == 0 {
|
|
return
|
|
}
|
|
|
|
for trackNo, track := range g.Tracks {
|
|
nonEmptySegments := make([]GPXTrackSegment, 0)
|
|
for _, segment := range track.Segments {
|
|
if len(segment.Points) > 0 {
|
|
//fmt.Printf("Valid segment, because of %d points!\n", len(segment.Points))
|
|
nonEmptySegments = append(nonEmptySegments, segment)
|
|
}
|
|
}
|
|
g.Tracks[trackNo].Segments = nonEmptySegments
|
|
}
|
|
|
|
nonEmptyTracks := make([]GPXTrack, 0)
|
|
for _, track := range g.Tracks {
|
|
if len(track.Segments) > 0 {
|
|
//fmt.Printf("Valid track, baceuse of %d segments!\n", len(track.Segments))
|
|
nonEmptyTracks = append(nonEmptyTracks, track)
|
|
}
|
|
}
|
|
g.Tracks = nonEmptyTracks
|
|
}
|
|
|
|
func (g *GPX) SmoothHorizontal() {
|
|
for trackNo := range g.Tracks {
|
|
g.Tracks[trackNo].SmoothHorizontal()
|
|
}
|
|
}
|
|
|
|
func (g *GPX) SmoothVertical() {
|
|
for trackNo := range g.Tracks {
|
|
g.Tracks[trackNo].SmoothVertical()
|
|
}
|
|
}
|
|
|
|
func (g *GPX) RemoveHorizontalExtremes() {
|
|
for trackNo := range g.Tracks {
|
|
g.Tracks[trackNo].RemoveHorizontalExtremes()
|
|
}
|
|
}
|
|
|
|
func (g *GPX) RemoveVerticalExtremes() {
|
|
for trackNo := range g.Tracks {
|
|
g.Tracks[trackNo].RemoveVerticalExtremes()
|
|
}
|
|
}
|
|
|
|
func (g *GPX) AddMissingTime() {
|
|
for trackNo := range g.Tracks {
|
|
g.Tracks[trackNo].AddMissingTime()
|
|
}
|
|
}
|
|
|
|
func (g *GPX) AppendTrack(t *GPXTrack) {
|
|
g.Tracks = append(g.Tracks, *t)
|
|
}
|
|
|
|
// Append segment on end of track, of not track exists an empty one will be added.
|
|
func (g *GPX) AppendSegment(s *GPXTrackSegment) {
|
|
if len(g.Tracks) == 0 {
|
|
g.AppendTrack(new(GPXTrack))
|
|
}
|
|
g.Tracks[len(g.Tracks)-1].AppendSegment(s)
|
|
}
|
|
|
|
// Append segment on end of track, of not tracks/segments exists an empty one will be added.
|
|
func (g *GPX) AppendPoint(p *GPXPoint) {
|
|
if len(g.Tracks) == 0 {
|
|
g.AppendTrack(new(GPXTrack))
|
|
}
|
|
|
|
lastTrack := &g.Tracks[len(g.Tracks)-1]
|
|
if len(lastTrack.Segments) == 0 {
|
|
lastTrack.AppendSegment(new(GPXTrackSegment))
|
|
}
|
|
|
|
lastSegment := &lastTrack.Segments[len(lastTrack.Segments)-1]
|
|
|
|
lastSegment.AppendPoint(p)
|
|
}
|
|
|
|
func (g *GPX) AppendRoute(r *GPXRoute) {
|
|
g.Routes = append(g.Routes, *r)
|
|
}
|
|
|
|
func (g *GPX) AppendWaypoint(w *GPXPoint) {
|
|
g.Waypoints = append(g.Waypoints, *w)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type ElevationBounds struct {
|
|
MinElevation float64
|
|
MaxElevation float64
|
|
}
|
|
|
|
// Equals returns true if two Bounds objects are equal
|
|
func (b ElevationBounds) Equals(b2 ElevationBounds) bool {
|
|
return b.MinElevation == b2.MinElevation && b.MaxElevation == b2.MaxElevation
|
|
}
|
|
|
|
func (b *ElevationBounds) String() string {
|
|
return fmt.Sprintf("Max: %+v Min: %+v", b.MinElevation, b.MaxElevation)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type GpxBounds struct {
|
|
MinLatitude float64
|
|
MaxLatitude float64
|
|
MinLongitude float64
|
|
MaxLongitude float64
|
|
}
|
|
|
|
// Equals returns true if two Bounds objects are equal
|
|
func (b GpxBounds) Equals(b2 GpxBounds) bool {
|
|
return b.MinLatitude == b2.MinLatitude && b.MaxLatitude == b2.MaxLatitude && b.MinLongitude == b2.MinLongitude && b.MaxLongitude == b2.MaxLongitude
|
|
}
|
|
|
|
func (b *GpxBounds) String() string {
|
|
return fmt.Sprintf("Max: %+v, %+v Min: %+v, %+v", b.MinLatitude, b.MinLongitude, b.MaxLatitude, b.MaxLongitude)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
// Generic point data
|
|
type Point struct {
|
|
Latitude float64
|
|
Longitude float64
|
|
Elevation NullableFloat64
|
|
}
|
|
|
|
func (pt *Point) GetLatitude() float64 {
|
|
return pt.Latitude
|
|
}
|
|
|
|
func (pt *Point) GetLongitude() float64 {
|
|
return pt.Longitude
|
|
}
|
|
|
|
func (pt *Point) GetElevation() NullableFloat64 {
|
|
return pt.Elevation
|
|
}
|
|
|
|
// Distance2D returns the 2D distance of two GpxWpts.
|
|
func (pt *Point) Distance2D(pt2 Location) float64 {
|
|
return Distance2D(pt.GetLatitude(), pt.GetLongitude(), pt2.GetLatitude(), pt2.GetLongitude(), false)
|
|
}
|
|
|
|
// Distance3D returns the 3D distance of two GpxWpts.
|
|
func (pt *Point) Distance3D(pt2 Location) float64 {
|
|
return Distance3D(pt.GetLatitude(), pt.GetLongitude(), pt.GetElevation(), pt2.GetLatitude(), pt2.GetLongitude(), pt2.GetElevation(), false)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type TimeBounds struct {
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
}
|
|
|
|
func (tb TimeBounds) Equals(tb2 TimeBounds) bool {
|
|
if tb.StartTime == tb2.StartTime && tb.EndTime == tb2.EndTime {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (tb *TimeBounds) String() string {
|
|
return fmt.Sprintf("%+v, %+v", tb.StartTime, tb.EndTime)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type UphillDownhill struct {
|
|
Uphill float64
|
|
Downhill float64
|
|
}
|
|
|
|
func (ud UphillDownhill) Equals(ud2 UphillDownhill) bool {
|
|
if ud.Uphill == ud2.Uphill && ud.Downhill == ud2.Downhill {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
// Position of a point on track
|
|
type TrackPosition struct {
|
|
Point
|
|
TrackNo int
|
|
SegmentNo int
|
|
PointNo int
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type GPXPoint struct {
|
|
Point
|
|
// TODO
|
|
Timestamp time.Time
|
|
// TODO: Type
|
|
MagneticVariation string
|
|
// TODO: Type
|
|
GeoidHeight string
|
|
// Description info
|
|
Name string
|
|
Comment string
|
|
Description string
|
|
Source string
|
|
// TODO
|
|
// Links []GpxLink
|
|
Symbol string
|
|
Type string
|
|
// Accuracy info
|
|
TypeOfGpsFix string
|
|
Satellites NullableInt
|
|
HorizontalDilution NullableFloat64
|
|
VerticalDilution NullableFloat64
|
|
PositionalDilution NullableFloat64
|
|
AgeOfDGpsData NullableFloat64
|
|
DGpsId NullableInt
|
|
}
|
|
|
|
// SpeedBetween calculates the speed between two GpxWpts.
|
|
func (pt *GPXPoint) SpeedBetween(pt2 *GPXPoint, threeD bool) float64 {
|
|
seconds := pt.TimeDiff(pt2)
|
|
var distLen float64
|
|
if threeD {
|
|
distLen = pt.Distance3D(pt2)
|
|
} else {
|
|
distLen = pt.Distance2D(pt2)
|
|
}
|
|
|
|
return distLen / seconds
|
|
}
|
|
|
|
// TimeDiff returns the time difference of two GpxWpts in seconds.
|
|
func (pt *GPXPoint) TimeDiff(pt2 *GPXPoint) float64 {
|
|
t1 := pt.Timestamp
|
|
t2 := pt2.Timestamp
|
|
|
|
if t1.Equal(t2) {
|
|
return 0.0
|
|
}
|
|
|
|
var delta time.Duration
|
|
if t1.After(t2) {
|
|
delta = t1.Sub(t2)
|
|
} else {
|
|
delta = t2.Sub(t1)
|
|
}
|
|
|
|
return delta.Seconds()
|
|
}
|
|
|
|
// MaxDilutionOfPrecision returns the dilution precision of a GpxWpt.
|
|
func (pt *GPXPoint) MaxDilutionOfPrecision() float64 {
|
|
return math.Max(pt.HorizontalDilution.Value(), math.Max(pt.VerticalDilution.Value(), pt.PositionalDilution.Value()))
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type GPXRoute struct {
|
|
Name string
|
|
Comment string
|
|
Description string
|
|
Source string
|
|
// TODO
|
|
//Links []Link
|
|
Number NullableInt
|
|
Type string
|
|
// TODO
|
|
Points []GPXPoint
|
|
}
|
|
|
|
// Length returns the length of a GPX route.
|
|
func (rte *GPXRoute) Length() float64 {
|
|
// TODO: npe check
|
|
points := make([]Point, len(rte.Points))
|
|
for pointNo, point := range rte.Points {
|
|
points[pointNo] = point.Point
|
|
}
|
|
return Length2D(points)
|
|
}
|
|
|
|
// Center returns the center of a GPX route.
|
|
func (rte *GPXRoute) Center() (float64, float64) {
|
|
lenRtePts := len(rte.Points)
|
|
if lenRtePts == 0 {
|
|
return 0.0, 0.0
|
|
}
|
|
|
|
var (
|
|
sumLat float64
|
|
sumLon float64
|
|
)
|
|
|
|
for _, pt := range rte.Points {
|
|
sumLat += pt.Latitude
|
|
sumLon += pt.Longitude
|
|
}
|
|
|
|
n := float64(lenRtePts)
|
|
return sumLat / n, sumLon / n
|
|
}
|
|
|
|
func (rte *GPXRoute) ExecuteOnPoints(executor func(*GPXPoint)) {
|
|
for pointNo := range rte.Points {
|
|
executor(&rte.Points[pointNo])
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type GPXTrackSegment struct {
|
|
Points []GPXPoint
|
|
// TODO extensions
|
|
}
|
|
|
|
// Length2D returns the 2D length of a GPX segment.
|
|
func (seg *GPXTrackSegment) Length2D() float64 {
|
|
// TODO: There should be a better way to do this:
|
|
points := make([]Point, len(seg.Points))
|
|
for pointNo, point := range seg.Points {
|
|
points[pointNo] = point.Point
|
|
}
|
|
return Length2D(points)
|
|
}
|
|
|
|
// Length3D returns the 3D length of a GPX segment.
|
|
func (seg *GPXTrackSegment) Length3D() float64 {
|
|
// TODO: There should be a better way to do this:
|
|
points := make([]Point, len(seg.Points))
|
|
for pointNo, point := range seg.Points {
|
|
points[pointNo] = point.Point
|
|
}
|
|
return Length3D(points)
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) GetTrackPointsNo() int {
|
|
return len(seg.Points)
|
|
}
|
|
|
|
// TimeBounds returns the time bounds of a GPX segment.
|
|
func (seg *GPXTrackSegment) TimeBounds() TimeBounds {
|
|
timeTuple := make([]time.Time, 0)
|
|
|
|
for _, trkpt := range seg.Points {
|
|
if len(timeTuple) < 2 {
|
|
timeTuple = append(timeTuple, trkpt.Timestamp)
|
|
} else {
|
|
timeTuple[1] = trkpt.Timestamp
|
|
}
|
|
}
|
|
|
|
if len(timeTuple) == 2 {
|
|
return TimeBounds{StartTime: timeTuple[0], EndTime: timeTuple[1]}
|
|
}
|
|
|
|
return TimeBounds{StartTime: time.Time{}, EndTime: time.Time{}}
|
|
}
|
|
|
|
// Bounds returns the bounds of a GPX segment.
|
|
func (seg *GPXTrackSegment) Bounds() GpxBounds {
|
|
minmax := getMaximalGpxBounds()
|
|
for _, pt := range seg.Points {
|
|
minmax.MaxLatitude = math.Max(pt.Latitude, minmax.MaxLatitude)
|
|
minmax.MinLatitude = math.Min(pt.Latitude, minmax.MinLatitude)
|
|
minmax.MaxLongitude = math.Max(pt.Longitude, minmax.MaxLongitude)
|
|
minmax.MinLongitude = math.Min(pt.Longitude, minmax.MinLongitude)
|
|
}
|
|
return minmax
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) ElevationBounds() ElevationBounds {
|
|
minmax := getMaximalElevationBounds()
|
|
for _, pt := range seg.Points {
|
|
if pt.Elevation.NotNull() {
|
|
minmax.MaxElevation = math.Max(pt.Elevation.Value(), minmax.MaxElevation)
|
|
minmax.MinElevation = math.Min(pt.Elevation.Value(), minmax.MinElevation)
|
|
}
|
|
}
|
|
return minmax
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) HasTimes() bool {
|
|
return false
|
|
/*
|
|
withTimes := 0
|
|
for _, point := range seg.Points {
|
|
if point.Timestamp != nil {
|
|
withTimes += 1
|
|
}
|
|
}
|
|
return withTimes / len(seg.Points) >= 0.75
|
|
*/
|
|
}
|
|
|
|
// Speed returns the speed at point number in a GPX segment.
|
|
func (seg *GPXTrackSegment) Speed(pointIdx int) float64 {
|
|
trkptsLen := len(seg.Points)
|
|
if pointIdx >= trkptsLen {
|
|
pointIdx = trkptsLen - 1
|
|
}
|
|
|
|
point := seg.Points[pointIdx]
|
|
|
|
var prevPt *GPXPoint
|
|
var nextPt *GPXPoint
|
|
|
|
havePrev := false
|
|
haveNext := false
|
|
if 0 < pointIdx && pointIdx < trkptsLen {
|
|
prevPt = &seg.Points[pointIdx-1]
|
|
havePrev = true
|
|
}
|
|
|
|
if 0 < pointIdx && pointIdx < trkptsLen-1 {
|
|
nextPt = &seg.Points[pointIdx+1]
|
|
haveNext = true
|
|
}
|
|
|
|
haveSpeed1 := false
|
|
haveSpeed2 := false
|
|
|
|
var speed1 float64
|
|
var speed2 float64
|
|
if havePrev {
|
|
speed1 = math.Abs(point.SpeedBetween(prevPt, true))
|
|
haveSpeed1 = true
|
|
}
|
|
if haveNext {
|
|
speed2 = math.Abs(point.SpeedBetween(nextPt, true))
|
|
haveSpeed2 = true
|
|
}
|
|
|
|
if haveSpeed1 && haveSpeed2 {
|
|
return (speed1 + speed2) / 2.0
|
|
}
|
|
|
|
if haveSpeed1 {
|
|
return speed1
|
|
}
|
|
return speed2
|
|
}
|
|
|
|
// Duration returns the duration in seconds in a GPX segment.
|
|
func (seg *GPXTrackSegment) Duration() float64 {
|
|
trksLen := len(seg.Points)
|
|
if trksLen == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
first := seg.Points[0]
|
|
last := seg.Points[trksLen-1]
|
|
|
|
firstTimestamp := first.Timestamp
|
|
lastTimestamp := last.Timestamp
|
|
|
|
if firstTimestamp.Equal(lastTimestamp) {
|
|
return 0.0
|
|
}
|
|
|
|
if lastTimestamp.Before(firstTimestamp) {
|
|
return 0.0
|
|
}
|
|
dur := lastTimestamp.Sub(firstTimestamp)
|
|
|
|
return dur.Seconds()
|
|
}
|
|
|
|
// Elevations returns a slice with the elevations in a GPX segment.
|
|
func (seg *GPXTrackSegment) Elevations() []NullableFloat64 {
|
|
elevations := make([]NullableFloat64, len(seg.Points))
|
|
for i, trkpt := range seg.Points {
|
|
elevations[i] = trkpt.Elevation
|
|
}
|
|
return elevations
|
|
}
|
|
|
|
// UphillDownhill returns uphill and dowhill in a GPX segment.
|
|
func (seg *GPXTrackSegment) UphillDownhill() UphillDownhill {
|
|
if len(seg.Points) == 0 {
|
|
return UphillDownhill{Uphill: 0.0, Downhill: 0.0}
|
|
}
|
|
|
|
elevations := seg.Elevations()
|
|
|
|
uphill, downhill := CalcUphillDownhill(elevations)
|
|
|
|
return UphillDownhill{Uphill: uphill, Downhill: downhill}
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) ExecuteOnPoints(executor func(*GPXPoint)) {
|
|
for pointNo := range seg.Points {
|
|
executor(&seg.Points[pointNo])
|
|
}
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) ReduceTrackPoints(minDistance float64) {
|
|
if minDistance <= 0 {
|
|
return
|
|
}
|
|
|
|
if len(seg.Points) <= 1 {
|
|
return
|
|
}
|
|
|
|
newPoints := make([]GPXPoint, 0)
|
|
newPoints = append(newPoints, seg.Points[0])
|
|
|
|
for _, point := range seg.Points {
|
|
previousPoint := newPoints[len(newPoints)-1]
|
|
if point.Distance3D(&previousPoint) >= minDistance {
|
|
newPoints = append(newPoints, point)
|
|
}
|
|
}
|
|
|
|
seg.Points = newPoints
|
|
}
|
|
|
|
// Does Ramer-Douglas-Peucker algorithm for simplification of polyline
|
|
func (seg *GPXTrackSegment) SimplifyTracks(maxDistance float64) {
|
|
seg.Points = simplifyPoints(seg.Points, maxDistance)
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) AddElevation(elevation float64) {
|
|
for _, point := range seg.Points {
|
|
if point.Elevation.NotNull() {
|
|
point.Elevation.SetValue(point.Elevation.Value() + elevation)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Split splits a GPX segment at point index pt. Point pt remains in
|
|
// first part.
|
|
func (seg *GPXTrackSegment) Split(pt int) (*GPXTrackSegment, *GPXTrackSegment) {
|
|
pts1 := seg.Points[:pt+1]
|
|
pts2 := seg.Points[pt+1:]
|
|
|
|
return &GPXTrackSegment{Points: pts1}, &GPXTrackSegment{Points: pts2}
|
|
}
|
|
|
|
// Join concatenates to GPX segments.
|
|
func (seg *GPXTrackSegment) Join(seg2 *GPXTrackSegment) {
|
|
seg.Points = append(seg.Points, seg2.Points...)
|
|
}
|
|
|
|
// PositionAt returns the GpxWpt at a given time.
|
|
func (seg *GPXTrackSegment) PositionAt(t time.Time) int {
|
|
lenPts := len(seg.Points)
|
|
if lenPts == 0 {
|
|
return -1
|
|
}
|
|
first := seg.Points[0]
|
|
last := seg.Points[lenPts-1]
|
|
|
|
firstTimestamp := first.Timestamp
|
|
lastTimestamp := last.Timestamp
|
|
|
|
if firstTimestamp.Equal(lastTimestamp) || firstTimestamp.After(lastTimestamp) {
|
|
return -1
|
|
}
|
|
|
|
for i := 0; i < len(seg.Points); i++ {
|
|
pt := seg.Points[i]
|
|
if t.Before(pt.Timestamp) {
|
|
return i
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) StoppedPositions() []TrackPosition {
|
|
result := make([]TrackPosition, 0)
|
|
for pointNo, point := range seg.Points {
|
|
if pointNo > 0 {
|
|
previousPoint := seg.Points[pointNo-1]
|
|
if point.SpeedBetween(&previousPoint, true) < DEFAULT_STOPPED_SPEED_THRESHOLD {
|
|
var trackPos TrackPosition
|
|
trackPos.Point = point.Point
|
|
trackPos.PointNo = pointNo
|
|
trackPos.SegmentNo = -1
|
|
trackPos.TrackNo = -1
|
|
result = append(result, trackPos)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MovingData returns the moving data of a GPX segment.
|
|
func (seg *GPXTrackSegment) MovingData() MovingData {
|
|
var (
|
|
movingTime float64
|
|
stoppedTime float64
|
|
movingDistance float64
|
|
stoppedDistance float64
|
|
)
|
|
|
|
speedsDistances := make([]SpeedsAndDistances, 0)
|
|
|
|
for i := 1; i < len(seg.Points); i++ {
|
|
prev := seg.Points[i-1]
|
|
pt := seg.Points[i]
|
|
|
|
dist := pt.Distance3D(&prev)
|
|
|
|
timedelta := pt.Timestamp.Sub(prev.Timestamp)
|
|
seconds := timedelta.Seconds()
|
|
var speedKmh float64
|
|
|
|
if seconds > 0 {
|
|
speedKmh = (dist / 1000.0) / (timedelta.Seconds() / math.Pow(60, 2))
|
|
}
|
|
|
|
if speedKmh <= DEFAULT_STOPPED_SPEED_THRESHOLD {
|
|
stoppedTime += timedelta.Seconds()
|
|
stoppedDistance += dist
|
|
} else {
|
|
movingTime += timedelta.Seconds()
|
|
movingDistance += dist
|
|
|
|
sd := SpeedsAndDistances{dist / timedelta.Seconds(), dist}
|
|
speedsDistances = append(speedsDistances, sd)
|
|
}
|
|
}
|
|
|
|
var maxSpeed float64
|
|
if len(speedsDistances) > 0 {
|
|
maxSpeed = CalcMaxSpeed(speedsDistances)
|
|
if math.IsNaN(maxSpeed) {
|
|
maxSpeed = 0
|
|
}
|
|
}
|
|
|
|
return MovingData{
|
|
movingTime,
|
|
stoppedTime,
|
|
movingDistance,
|
|
stoppedDistance,
|
|
maxSpeed,
|
|
}
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) AppendPoint(p *GPXPoint) {
|
|
seg.Points = append(seg.Points, *p)
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) SmoothVertical() {
|
|
seg.Points = smoothVertical(seg.Points)
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) SmoothHorizontal() {
|
|
seg.Points = smoothHorizontal(seg.Points)
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) RemoveVerticalExtremes() {
|
|
if len(seg.Points) < REMOVE_EXTREEMES_TRESHOLD {
|
|
return
|
|
}
|
|
|
|
elevationDeltaSum := 0.0
|
|
elevationDeltaNo := 0
|
|
for pointNo, point := range seg.Points {
|
|
if pointNo > 0 && point.Elevation.NotNull() && seg.Points[pointNo-1].Elevation.NotNull() {
|
|
elevationDeltaSum += math.Abs(point.Elevation.Value() - seg.Points[pointNo-1].Elevation.Value())
|
|
elevationDeltaNo += 1
|
|
}
|
|
}
|
|
avgElevationDelta := elevationDeltaSum / float64(elevationDeltaNo)
|
|
removeElevationExtremesThreshold := avgElevationDelta * 5.0
|
|
|
|
smoothedPoints := smoothVertical(seg.Points)
|
|
originalPoints := seg.Points
|
|
|
|
newPoints := make([]GPXPoint, 0)
|
|
for pointNo, point := range originalPoints {
|
|
smoothedPoint := smoothedPoints[pointNo]
|
|
if 0 < pointNo && pointNo < len(originalPoints)-1 && point.Elevation.NotNull() && smoothedPoints[pointNo].Elevation.NotNull() {
|
|
d := originalPoints[pointNo-1].Distance3D(&originalPoints[pointNo+1])
|
|
d1 := originalPoints[pointNo].Distance3D(&originalPoints[pointNo-1])
|
|
d2 := originalPoints[pointNo].Distance3D(&originalPoints[pointNo+1])
|
|
if d1+d2 > d*1.5 {
|
|
if math.Abs(point.Elevation.Value()-smoothedPoint.Elevation.Value()) < removeElevationExtremesThreshold {
|
|
newPoints = append(newPoints, point)
|
|
}
|
|
} else {
|
|
newPoints = append(newPoints, point)
|
|
}
|
|
} else {
|
|
newPoints = append(newPoints, point)
|
|
}
|
|
}
|
|
seg.Points = newPoints
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) RemoveHorizontalExtremes() {
|
|
// Dont't remove extreemes if segment too small
|
|
if len(seg.Points) < REMOVE_EXTREEMES_TRESHOLD {
|
|
return
|
|
}
|
|
|
|
var sum float64
|
|
for pointNo, point := range seg.Points {
|
|
if pointNo > 0 {
|
|
sum += point.Distance2D(&seg.Points[pointNo-1])
|
|
}
|
|
}
|
|
// Division by zero not a problems since this is not computed on zero-length segments:
|
|
avgDistanceBetweenPoints := float64(sum) / float64(len(seg.Points)-1)
|
|
|
|
remove2dExtremesThreshold := 1.75 * avgDistanceBetweenPoints
|
|
|
|
smoothedPoints := smoothHorizontal(seg.Points)
|
|
originalPoints := seg.Points
|
|
|
|
newPoints := make([]GPXPoint, 0)
|
|
for pointNo, point := range originalPoints {
|
|
if 0 < pointNo && pointNo < len(originalPoints)-1 {
|
|
d := originalPoints[pointNo-1].Distance2D(&originalPoints[pointNo+1])
|
|
d1 := originalPoints[pointNo].Distance2D(&originalPoints[pointNo-1])
|
|
d2 := originalPoints[pointNo].Distance2D(&originalPoints[pointNo+1])
|
|
if d1+d2 > d*1.5 {
|
|
pointMovedBy := smoothedPoints[pointNo].Distance2D(&point)
|
|
if pointMovedBy < remove2dExtremesThreshold {
|
|
newPoints = append(newPoints, point)
|
|
} else {
|
|
// Removed!
|
|
}
|
|
} else {
|
|
newPoints = append(newPoints, point)
|
|
}
|
|
} else {
|
|
newPoints = append(newPoints, point)
|
|
}
|
|
}
|
|
seg.Points = newPoints
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) AddMissingTime() {
|
|
emptySegmentStart := -1
|
|
for pointNo := range seg.Points {
|
|
timestampEmpty := seg.Points[pointNo].Timestamp.Year() <= 1
|
|
if timestampEmpty {
|
|
if emptySegmentStart == -1 {
|
|
emptySegmentStart = pointNo
|
|
}
|
|
} else {
|
|
if 0 < emptySegmentStart && pointNo < len(seg.Points) {
|
|
seg.addMissingTimeInSegment(emptySegmentStart, pointNo-1)
|
|
}
|
|
emptySegmentStart = -1
|
|
}
|
|
}
|
|
}
|
|
|
|
func (seg *GPXTrackSegment) addMissingTimeInSegment(start, end int) {
|
|
if start <= 0 {
|
|
return
|
|
}
|
|
if end >= len(seg.Points)-1 {
|
|
return
|
|
}
|
|
startTime, endTime := seg.Points[start-1].Timestamp, seg.Points[end+1].Timestamp
|
|
ratios := make([]float64, end-start+1)
|
|
|
|
length := 0.0
|
|
for i := start; i <= end; i++ {
|
|
length += seg.Points[i].Point.Distance2D(&seg.Points[i-1])
|
|
ratios[i-start] = length
|
|
}
|
|
length += seg.Points[end].Point.Distance2D(&seg.Points[end+1])
|
|
for i := start; i <= end; i++ {
|
|
ratios[i-start] = ratios[i-start] / length
|
|
}
|
|
|
|
for i := start; i <= end; i++ {
|
|
d := int64(ratios[i-start] * float64(endTime.Sub(startTime).Nanoseconds()))
|
|
seg.Points[i].Timestamp = startTime.Add(time.Duration(d))
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
type GPXTrack struct {
|
|
Name string
|
|
Comment string
|
|
Description string
|
|
Source string
|
|
// TODO
|
|
//Links []Link
|
|
Number NullableInt
|
|
Type string
|
|
Segments []GPXTrackSegment
|
|
}
|
|
|
|
// Length2D returns the 2D length of a GPX track.
|
|
func (trk *GPXTrack) Length2D() float64 {
|
|
var l float64
|
|
for _, seg := range trk.Segments {
|
|
d := seg.Length2D()
|
|
l += d
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Length3D returns the 3D length of a GPX track.
|
|
func (trk *GPXTrack) Length3D() float64 {
|
|
var l float64
|
|
for _, seg := range trk.Segments {
|
|
d := seg.Length3D()
|
|
l += d
|
|
}
|
|
return l
|
|
}
|
|
|
|
func (trk *GPXTrack) GetTrackPointsNo() int {
|
|
result := 0
|
|
for _, segment := range trk.Segments {
|
|
result += segment.GetTrackPointsNo()
|
|
}
|
|
return result
|
|
}
|
|
|
|
// TimeBounds returns the time bounds of a GPX track.
|
|
func (trk *GPXTrack) TimeBounds() TimeBounds {
|
|
var tbTrk TimeBounds
|
|
|
|
for i, seg := range trk.Segments {
|
|
tbSeg := seg.TimeBounds()
|
|
if i == 0 {
|
|
tbTrk = tbSeg
|
|
} else {
|
|
tbTrk.EndTime = tbSeg.EndTime
|
|
}
|
|
}
|
|
return tbTrk
|
|
}
|
|
|
|
// Bounds returns the bounds of a GPX track.
|
|
func (trk *GPXTrack) Bounds() GpxBounds {
|
|
minmax := getMaximalGpxBounds()
|
|
for _, seg := range trk.Segments {
|
|
bnds := seg.Bounds()
|
|
minmax.MaxLatitude = math.Max(bnds.MaxLatitude, minmax.MaxLatitude)
|
|
minmax.MinLatitude = math.Min(bnds.MinLatitude, minmax.MinLatitude)
|
|
minmax.MaxLongitude = math.Max(bnds.MaxLongitude, minmax.MaxLongitude)
|
|
minmax.MinLongitude = math.Min(bnds.MinLongitude, minmax.MinLongitude)
|
|
}
|
|
return minmax
|
|
}
|
|
|
|
func (trk *GPXTrack) ElevationBounds() ElevationBounds {
|
|
minmax := getMaximalElevationBounds()
|
|
for _, seg := range trk.Segments {
|
|
bnds := seg.ElevationBounds()
|
|
minmax.MaxElevation = math.Max(bnds.MaxElevation, minmax.MaxElevation)
|
|
minmax.MinElevation = math.Min(bnds.MinElevation, minmax.MinElevation)
|
|
}
|
|
return minmax
|
|
}
|
|
|
|
func (trk *GPXTrack) HasTimes() bool {
|
|
result := true
|
|
for _, segment := range trk.Segments {
|
|
result = result && segment.HasTimes()
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (trk *GPXTrack) ReduceTrackPoints(minDistance float64) {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].ReduceTrackPoints(minDistance)
|
|
}
|
|
}
|
|
|
|
func (trk *GPXTrack) SimplifyTracks(maxDistance float64) {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].SimplifyTracks(maxDistance)
|
|
}
|
|
}
|
|
|
|
// Split splits a GPX segment at a point number ptNo in a GPX track.
|
|
func (trk *GPXTrack) Split(segNo, ptNo int) {
|
|
lenSegs := len(trk.Segments)
|
|
if segNo >= lenSegs {
|
|
return
|
|
}
|
|
|
|
newSegs := make([]GPXTrackSegment, 0)
|
|
for i := 0; i < lenSegs; i++ {
|
|
seg := trk.Segments[i]
|
|
|
|
if i == segNo && ptNo < len(seg.Points) {
|
|
seg1, seg2 := seg.Split(ptNo)
|
|
newSegs = append(newSegs, *seg1, *seg2)
|
|
} else {
|
|
newSegs = append(newSegs, seg)
|
|
}
|
|
}
|
|
trk.Segments = newSegs
|
|
}
|
|
|
|
func (trk *GPXTrack) ExecuteOnPoints(executor func(*GPXPoint)) {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].ExecuteOnPoints(executor)
|
|
}
|
|
}
|
|
|
|
func (trk *GPXTrack) AddElevation(elevation float64) {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].AddElevation(elevation)
|
|
}
|
|
}
|
|
|
|
// Join joins two GPX segments in a GPX track.
|
|
func (trk *GPXTrack) Join(segNo, segNo2 int) {
|
|
lenSegs := len(trk.Segments)
|
|
if segNo >= lenSegs && segNo2 >= lenSegs {
|
|
return
|
|
}
|
|
newSegs := make([]GPXTrackSegment, 0)
|
|
for i := 0; i < lenSegs; i++ {
|
|
seg := trk.Segments[i]
|
|
if i == segNo {
|
|
secondSeg := trk.Segments[segNo2]
|
|
seg.Join(&secondSeg)
|
|
newSegs = append(newSegs, seg)
|
|
} else if i == segNo2 {
|
|
// do nothing, its already joined
|
|
} else {
|
|
newSegs = append(newSegs, seg)
|
|
}
|
|
}
|
|
trk.Segments = newSegs
|
|
}
|
|
|
|
// JoinNext joins a GPX segment with the next segment in the current GPX
|
|
// track.
|
|
func (trk *GPXTrack) JoinNext(segNo int) {
|
|
trk.Join(segNo, segNo+1)
|
|
}
|
|
|
|
// MovingData returns the moving data of a GPX track.
|
|
func (trk *GPXTrack) MovingData() MovingData {
|
|
var (
|
|
movingTime float64
|
|
stoppedTime float64
|
|
movingDistance float64
|
|
stoppedDistance float64
|
|
maxSpeed float64
|
|
)
|
|
|
|
for _, seg := range trk.Segments {
|
|
md := seg.MovingData()
|
|
movingTime += md.MovingTime
|
|
stoppedTime += md.StoppedTime
|
|
movingDistance += md.MovingDistance
|
|
stoppedDistance += md.StoppedDistance
|
|
|
|
if md.MaxSpeed > maxSpeed {
|
|
maxSpeed = md.MaxSpeed
|
|
}
|
|
}
|
|
return MovingData{
|
|
MovingTime: movingTime,
|
|
MovingDistance: movingDistance,
|
|
StoppedTime: stoppedTime,
|
|
StoppedDistance: stoppedDistance,
|
|
MaxSpeed: maxSpeed,
|
|
}
|
|
}
|
|
|
|
// Duration returns the duration of a GPX track.
|
|
func (trk *GPXTrack) Duration() float64 {
|
|
if len(trk.Segments) == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
var result float64
|
|
for _, seg := range trk.Segments {
|
|
result += seg.Duration()
|
|
}
|
|
return result
|
|
}
|
|
|
|
// UphillDownhill return the uphill and downhill values of a GPX track.
|
|
func (trk *GPXTrack) UphillDownhill() UphillDownhill {
|
|
if len(trk.Segments) == 0 {
|
|
return UphillDownhill{
|
|
Uphill: 0,
|
|
Downhill: 0,
|
|
}
|
|
}
|
|
|
|
var (
|
|
uphill float64
|
|
downhill float64
|
|
)
|
|
|
|
for _, seg := range trk.Segments {
|
|
updo := seg.UphillDownhill()
|
|
|
|
uphill += updo.Uphill
|
|
downhill += updo.Downhill
|
|
}
|
|
|
|
return UphillDownhill{
|
|
Uphill: uphill,
|
|
Downhill: downhill,
|
|
}
|
|
}
|
|
|
|
// PositionAt returns a LocationResultsPair for a given time.
|
|
func (trk *GPXTrack) PositionAt(t time.Time) []TrackPosition {
|
|
results := make([]TrackPosition, 0)
|
|
|
|
for i := 0; i < len(trk.Segments); i++ {
|
|
seg := trk.Segments[i]
|
|
loc := seg.PositionAt(t)
|
|
if loc != -1 {
|
|
results = append(results, TrackPosition{SegmentNo: i, PointNo: loc, Point: seg.Points[loc].Point})
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
func (trk *GPXTrack) StoppedPositions() []TrackPosition {
|
|
result := make([]TrackPosition, 0)
|
|
for segmentNo, segment := range trk.Segments {
|
|
positions := segment.StoppedPositions()
|
|
for _, position := range positions {
|
|
position.SegmentNo = segmentNo
|
|
result = append(result, position)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (trk *GPXTrack) AppendSegment(s *GPXTrackSegment) {
|
|
trk.Segments = append(trk.Segments, *s)
|
|
}
|
|
|
|
func (trk *GPXTrack) SmoothVertical() {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].SmoothVertical()
|
|
}
|
|
}
|
|
|
|
func (trk *GPXTrack) SmoothHorizontal() {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].SmoothHorizontal()
|
|
}
|
|
}
|
|
|
|
func (trk *GPXTrack) RemoveVerticalExtremes() {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].RemoveVerticalExtremes()
|
|
}
|
|
}
|
|
|
|
func (trk *GPXTrack) RemoveHorizontalExtremes() {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].RemoveHorizontalExtremes()
|
|
}
|
|
}
|
|
|
|
func (trk *GPXTrack) AddMissingTime() {
|
|
for segmentNo := range trk.Segments {
|
|
trk.Segments[segmentNo].AddMissingTime()
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Useful when looking for smaller bounds
|
|
*
|
|
* TODO does it work is region is between 179E and 179W?
|
|
*/
|
|
func getMaximalGpxBounds() GpxBounds {
|
|
return GpxBounds{
|
|
MaxLatitude: -math.MaxFloat64,
|
|
MinLatitude: math.MaxFloat64,
|
|
MaxLongitude: -math.MaxFloat64,
|
|
MinLongitude: math.MaxFloat64,
|
|
}
|
|
}
|
|
|
|
func getMaximalElevationBounds() ElevationBounds {
|
|
return ElevationBounds{
|
|
MaxElevation: -math.MaxFloat64,
|
|
MinElevation: math.MaxFloat64,
|
|
}
|
|
}
|