1
0
Fork 0
mirror of https://github.com/Luzifer/staticmap.git synced 2024-10-19 16:14:22 +00:00
staticmap/vendor/github.com/tkrajina/gpxgo/gpx/gpx.go
Knut Ahlers 759b968510
Vendor dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2017-06-27 22:50:36 +02:00

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,
}
}