diff options
Diffstat (limited to 'vendor/github.com/tkrajina/gpxgo/gpx/gpx.go')
| -rw-r--r-- | vendor/github.com/tkrajina/gpxgo/gpx/gpx.go | 1582 |
1 files changed, 1582 insertions, 0 deletions
diff --git a/vendor/github.com/tkrajina/gpxgo/gpx/gpx.go b/vendor/github.com/tkrajina/gpxgo/gpx/gpx.go new file mode 100644 index 0000000..7646aa4 --- /dev/null +++ b/vendor/github.com/tkrajina/gpxgo/gpx/gpx.go @@ -0,0 +1,1582 @@ +// 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, + } +} |
