public static IGPSPoint getGpsLoc(ZoneFiveSoftware.Common.Data.Fitness.IActivity activity, DateTime time) { IGPSPoint result = null; if (activity != null && activity.GPSRoute != null && activity.GPSRoute.Count > 0) { ITimeValueEntry <IGPSPoint> p = activity.GPSRoute.GetInterpolatedValue(time); if (null != p) { result = p.Value; } else { //This can happen if the activity starts before the GPS track if (time <= activity.GPSRoute.StartTime) { result = activity.GPSRoute[0].Value; } else if (time > activity.GPSRoute.EntryDateTime(activity.GPSRoute[activity.GPSRoute.Count - 1])) { result = activity.GPSRoute[activity.GPSRoute.Count - 1].Value; } } } return(result); }
/// <summary> /// Gets the 'critical value' for a particular activity. For instance, 5 minute power in an given activity. /// </summary> /// <param name="activity">Activity to analyze</param> /// <param name="chartType">Which data track to analyze</param> /// <param name="seconds">Seconds representing the period. For example, 5 minute power would be 300 seconds.</param> /// <returns>The critical value requested. 5 minute power might return 350 watts for example.</returns> internal static float GetCriticalValue(IActivity activity, Common.TrackType chartType, float seconds) { float min, criticalValue; INumericTimeDataSeries source, mmTrack, timeTrack; string id = activity.ReferenceId + chartType + seconds; mmTrack = GetMeanMaxTrack(activity, chartType, out timeTrack, false); // Return value from cache if (criticalCache.ContainsKey(id)) { // Critical cache return(criticalCache[id]); } else if (mmTrack != null) { // value was cached previously. saves a little time. ITimeValueEntry <float> point = mmTrack.GetInterpolatedValue(mmTrack.StartTime.AddSeconds(seconds)); if (point != null) { criticalValue = point.Value; } else { // Value does not exist for this activity criticalValue = float.NaN; } } else { // Calculate value switch (chartType) { case Common.TrackType.Cadence: source = activity.CadencePerMinuteTrack; break; case Common.TrackType.Power: source = activity.PowerWattsTrack; break; default: case Common.TrackType.HR: source = activity.HeartRatePerMinuteTrack; break; } Utilities.Smooth(source, (uint)seconds, out min, out criticalValue); } // Save to cache if (!float.IsNaN(criticalValue) && criticalValue != 0) { criticalCache.Add(id, criticalValue); } return(criticalValue); }
/// <summary> /// Combine a list of tracks by averaging all of the values together. /// Start times are ignored, ElapsedSeconds is used for track alignment. Each track is weighted equally. /// </summary> /// <param name="tracks">List of tracks to average together</param> /// <returns>A single track representing the average data series.</returns> private static INumericTimeDataSeries CombineAvgTracks(IList <INumericTimeDataSeries> tracks) { // Quick data analysis... return quickly if possible if (tracks.Count == 0) { // No data to analyze return(null); } else if (tracks.Count == 1) { // Only 1 track. It is it's own average :) return(tracks[0]); } // Setup for analysis... INumericTimeDataSeries avgTrack = new NumericTimeDataSeries(); INumericTimeDataSeries longTrack = tracks[0]; // Find longest track to be used as index later foreach (INumericTimeDataSeries track in tracks) { if (longTrack.TotalElapsedSeconds < track.TotalElapsedSeconds) { longTrack = track; } } // Average the values: Use longTrack as the reference because it'll cover all tracks // (some may be shorter, which is OK) // Using this track as a reference will also be more efficient because it // won't go through every second, it'll take advantage of the logarithmic algos elsewhere. foreach (ITimeValueEntry <float> index in longTrack) { // Sum & count used later to determine average float sum = 0; int count = 0; // Collect average point in each track foreach (INumericTimeDataSeries track in tracks) { ITimeValueEntry <float> item = track.GetInterpolatedValue(track.StartTime.AddSeconds(index.ElapsedSeconds)); if (item != null) { sum += item.Value; count++; } } // Store average avgTrack.Add(longTrack.EntryDateTime(index), sum / count); } return(avgTrack); }
private bool FillSingleDataSerie(LineChartTypes chartType, ChartDataSeries dataSerie) { INumericTimeDataSeries graphPoints = GetSmoothedActivityTrack(chartType); if (graphPoints.Count > 0) { TimeSpan trackStartDiffWithActivity = graphPoints.StartTime - m_ActivityInfoCache.ActualTrackStart; float trackSecondDifference = (float)trackStartDiffWithActivity.TotalSeconds; if (XAxisReferential == XAxisValue.Time) { graphPoints = Utils.Utils.RemovePausedTimesInTrack(graphPoints, Activity); foreach (ITimeValueEntry <float> entry in graphPoints) { float key = trackSecondDifference + entry.ElapsedSeconds; if (!dataSerie.Points.ContainsKey(key)) { dataSerie.Points.Add(key, new PointF(trackSecondDifference + entry.ElapsedSeconds, entry.Value)); } } } else if (m_ActivityInfoCache.MovingDistanceMetersTrack != null) { IDistanceDataTrack distanceTrack = m_ActivityInfoCache.MovingDistanceMetersTrack; int pointCount = Math.Min(distanceTrack.Count, graphPoints.Count); ITimeValueEntry <float> startEntry = distanceTrack.GetInterpolatedValue(graphPoints.StartTime); foreach (ITimeValueEntry <float> entry in graphPoints) { ITimeValueEntry <float> interpolatedEntry = distanceTrack.GetInterpolatedValue(graphPoints.StartTime + new TimeSpan(0, 0, (int)entry.ElapsedSeconds)); if (interpolatedEntry != null) { float distanceAtTime = interpolatedEntry.Value; float distanceValue = (float)Length.Convert(distanceAtTime, Length.Units.Meter, Activity.Category.DistanceUnits); float key = trackSecondDifference + entry.ElapsedSeconds; if (!dataSerie.Points.ContainsKey(key)) { dataSerie.Points.Add(key, new PointF(distanceValue, entry.Value)); } } } } return(true); } return(false); }
public static IList <IList <IGPSPoint> > GpsPoints(IGPSRoute gpsRoute, IValueRangeSeries <DateTime> selections) { IList <IList <IGPSPoint> > result = new List <IList <IGPSPoint> >(); if (selections != null && selections.Count > 0 && gpsRoute != null && gpsRoute.Count > 1) { //selection and gps points are sorted without overlap so traverse over gps points only once //(previous version had much more complicated version, that also accounted for pauses) int i = 0; foreach (IValueRange <DateTime> sel in selections) { IList <IGPSPoint> track = new List <IGPSPoint>(); //Use start/end "with priority", even if extra points are added. Care needed if returning GPSRoute DateTime t = DateTimeRangeSeries.Latest(sel.Lower, gpsRoute.StartTime); ITimeValueEntry <IGPSPoint> pt = gpsRoute.GetInterpolatedValue(t); if (pt != null) { track.Add(pt.Value); } while (i < gpsRoute.Count) { ITimeValueEntry <IGPSPoint> entry = gpsRoute[i]; DateTime time = gpsRoute.EntryDateTime(entry); i++; if (sel.Lower > time) { continue; } if (sel.Upper < time) { //Do not increase counter here, it could be needed i--; break; } track.Add(entry.Value); } t = DateTimeRangeSeries.Earliest(sel.Upper, gpsRoute.StartTime.AddSeconds(gpsRoute.TotalElapsedSeconds)); pt = gpsRoute.GetInterpolatedValue(t); if (pt != null) { track.Add(pt.Value); } result.Add(track); } } return(result); }
private static bool SumValues(ITimeValueEntry <float> p1, ITimeValueEntry <float> p2, ref double value, ref long secondsRemaining) { double spanSeconds = Math.Abs((double)p2.ElapsedSeconds - (double)p1.ElapsedSeconds); if (spanSeconds <= secondsRemaining) { value += (p1.Value + p2.Value) * spanSeconds; secondsRemaining -= (long)spanSeconds; return(true); } else { double percent = (double)secondsRemaining / (double)spanSeconds; value += (p1.Value * ((float)2 - percent) + p2.Value * percent) * secondsRemaining; secondsRemaining = 0; return(false); } }
public static void InsertValue <T>(DateTime atime, ITimeDataSeries <T> track, ITimeDataSeries <T> source) { //Interpolation is down to seconds //TBD: Inefficient, source.IndexOf often fails #if ST_3_1_5314_BUG //http://www.zonefivesoftware.com/sporttracks/forums/viewtopic.php?p=84638#p84638 //System.Exception: FindPosOnOrBefore: Didn't find element properly. try { #endif ITimeValueEntry <T> interpolatedP = source.GetInterpolatedValue(atime); if (interpolatedP != null) { int index = source.IndexOf(interpolatedP); T val = interpolatedP.Value; if (index >= 0) { val = TrackUtil.getValFromDateTimeIndex(source, atime, index); } else { } try { #if !NO_ST_INSERT_START_TIME //ST bug: not inserted in order if ms differs for start if (Math.Abs((atime - track.StartTime).TotalSeconds) < 1) { track.RemoveAt(0); } #endif track.Add(atime, val); //T val2 = track.GetInterpolatedValue(atime).Value; } catch { } } #if ST_3_1_5314_BUG } catch (Exception e) { } #endif }
internal static float getValFromDateTime(INumericTimeDataSeries track, DateTime t, out int status) { //Ignore malformed activities and selection outside the result float res = 0; status = 1; ITimeValueEntry <float> entry = track.GetInterpolatedValue(t); if (entry != null) { res = entry.Value; status = 0; } else if (track.Count > 0 && t >= track.StartTime.AddSeconds(2)) { //Seem to be out of bounds //Any time after start is handled as end, as pauses may complicate calcs (default is 0, i.e. before) res = track[track.Count - 1].Value; } return(res); }
public static void ExtractRangeFromDataTrack(INumericTimeDataSeries sourceTrack, ValueRange <DateTime> timeToKeep, INumericTimeDataSeries resultTrack) { resultTrack.Clear(); if (sourceTrack != null && sourceTrack.Count > 0) { TimeSpan endElapsedTime = timeToKeep.Upper - sourceTrack.StartTime; TimeSpan totalElapsedTime = timeToKeep.Upper - timeToKeep.Lower; int endElapsedSeconds = (int)endElapsedTime.TotalSeconds; int totalElapsedSeconds = (int)totalElapsedTime.TotalSeconds; DateTime timeStart = timeToKeep.Lower; DateTime timeEnd = timeToKeep.Upper; DateTime sourceDataEndTime = sourceTrack.StartTime + new TimeSpan(0, 0, (int)sourceTrack.TotalElapsedSeconds); // Readjust to match the track since it can start after the activity start time // or end before the end time if (timeStart < sourceTrack.StartTime) { timeStart = sourceTrack.StartTime; } else if (timeStart > sourceDataEndTime) { timeStart = sourceDataEndTime; } if (timeEnd < sourceTrack.StartTime) { timeEnd = sourceTrack.StartTime; } else if (timeEnd > sourceDataEndTime) { timeEnd = sourceDataEndTime; } ITimeValueEntry <float> startDistance = sourceTrack.GetInterpolatedValue(timeStart); int currentItemIndex = sourceTrack.IndexOf(startDistance); // Since we use an interpolated value, it is possible that there is no matching // index, in which case we add it so we can have a start point in the array if (currentItemIndex == -1) { sourceTrack.Add(timeStart, startDistance.Value); startDistance = sourceTrack.GetInterpolatedValue(timeStart); currentItemIndex = sourceTrack.IndexOf(startDistance); } // Just make sure we have a start index. Can be == -1 if there is no track? if (currentItemIndex != -1) { // Now go through all indexes until we hit the end while (currentItemIndex < sourceTrack.Count && sourceTrack[currentItemIndex].ElapsedSeconds <= endElapsedSeconds) { ITimeValueEntry <float> currentDistance = sourceTrack[currentItemIndex]; DateTime currentTime = timeStart + new TimeSpan(0, 0, (int)(currentDistance.ElapsedSeconds - startDistance.ElapsedSeconds)); resultTrack.Add(currentTime, currentDistance.Value); ++currentItemIndex; } } // Since we check the elapsed seconds, it is possible that our end point needs to // be interpolated. Let's make sure we have our end point in our cached data ITimeValueEntry <float> lastEntry = sourceTrack.GetInterpolatedValue(timeEnd); // Should never be empty, we at least added the start above... if (resultTrack.Count != 0 && lastEntry != null) { if (resultTrack[resultTrack.Count - 1].ElapsedSeconds != totalElapsedSeconds) { // Add our final interpolated point resultTrack.Add(timeEnd, lastEntry.Value); } } } }
/// <summary> /// Gets an average value over a specific timeframe in an activity track. /// Used to calculate associated values, for example. /// </summary> /// <param name="activity">Activity to analyze</param> /// <param name="pointType">Which data track is data requested from</param> /// <param name="start">Start time of requested average period (in UTC time)</param> /// <param name="period">Duration of time (in seconds) to evaluate with respect to start time</param> /// <returns>The average value of X track, from 'start' to 'period' seconds later.</returns> internal static float GetAvgValue(IActivity activity, Common.TrackType pointType, DateTime start, uint period) { // Check for bad data if (activity == null || period < 0) { return(float.NaN); } INumericTimeDataSeries track = null; switch (pointType) { case Common.TrackType.Cadence: track = activity.CadencePerMinuteTrack; break; case Common.TrackType.HR: track = activity.HeartRatePerMinuteTrack; break; case Common.TrackType.Power: track = activity.PowerWattsTrack; break; case Common.TrackType.Grade: track = new NumericTimeDataSeries(ActivityInfoCache.Instance.GetInfo(activity).SmoothedGradeTrack); for (int i = 0; i < track.Count; i++) { track.SetValueAt(i, track[i].Value * 100f); } break; } // No track data if (track == null) { return(float.NaN); } // Find average value: Sum entries every 1 second, then divide it out at the end float sum = 0; DateTime time = start; TimeSpan span = TimeSpan.Zero; while (time < start.AddSeconds(period)) { // Sum all values (we'll divide it out at the end) ITimeValueEntry <float> item = track.GetInterpolatedValue(time); // Ignore bad values... they're simply excluded from calculation // and the average is taken from a smaller subset of values if (item != null) { sum += item.Value; span = span.Add(TimeSpan.FromSeconds(1)); } time = time.AddSeconds(1); } // Divide sum by period to get the average value if (span.TotalSeconds > 0) { return(sum / (int)span.TotalSeconds); } else { // Bad data. Oops, requested time range didn't exist for the requested track. return(float.NaN); } }
//public IDistanceDataTrack GetSmoothedDistanceTrack(int seconds, out float min, out float max) //{ // IDistanceDataTrack distanceTrack; // if (record.Activity.DistanceMetersTrack != null) // { // // #1 Use Distance track from activity // distanceTrack = record.Activity.DistanceMetersTrack; // } // else // { // if (record.Activity.GPSRoute != null) // { // // #2 Otherwise create a distance track from GPS // distanceTrack = Utilities.CreateDistanceDataTrack(record.Activity); // return Utilities.STSmooth(distanceTrack, seconds, min, max); // } // else // { // // Else, no distance track, and cannot create one. // distanceTrack = new DistanceDataTrack(); // } // } //} //public INumericTimeDataSeries GetSmoothedGradeTrack(int seconds, out float min, out float max) //{ // NumericTimeDataSeries gradeTrack = new NumericTimeDataSeries(); // for (int i = 0; i < record.Activity.ElevationMetersTrack.Count; i++) // { // if (i == 0) // { // gradeTrack.Add(record.Activity.ElevationMetersTrack[i].ElapsedSeconds, 0); // } // } //} #region Constructors public Feature(IActivity activity, feature_type type, DateTime inStartTime, DateTime inEndTime) { startTime = inStartTime; endTime = inEndTime; added = false; hillNumber = 0; _feature_type = type; masterActivityID = activity.ReferenceId; // Default fill and line color fillColor = Color.FromArgb(125, 146, 94, 9); lineColor = Color.FromArgb(255, 146, 94, 9); lineWidth = 1; selectedColor = Color.Empty; routeWidth = PluginMain.GetApplication().SystemPreferences.RouteSettings.RouteWidth; IGPSRoute recordGPS = new GPSRoute(); INumericTimeDataSeries recordHRTrack = new NumericTimeDataSeries(); INumericTimeDataSeries pwrTrack = new NumericTimeDataSeries(); INumericTimeDataSeries elevTrack = new NumericTimeDataSeries(); INumericTimeDataSeries cadTrack = new NumericTimeDataSeries(); IDistanceDataTrack distTrack = new DistanceDataTrack(); RecordCategory category = new RecordCategory(); ActivityInfo ai = ActivityInfoCache.Instance.GetInfo(activity); DateTime start = activity.StartTime; if (activity.GPSRoute != null) { // Check and make sure the route has points if (activity.GPSRoute.Count > 0) { // If the time passed in is before the start of the gps track, get the first value if (activity.GPSRoute.StartTime > inStartTime) { startPoint = activity.GPSRoute[0].Value; } else { // Set the start point ITimeValueEntry <IGPSPoint> sPoint = activity.GPSRoute.GetInterpolatedValue(inStartTime); if (sPoint != null) { startPoint = sPoint.Value; } } // If the time passed in is after the end of the gps track, get the last value if (activity.GPSRoute.StartTime.AddSeconds(activity.GPSRoute[activity.GPSRoute.Count - 1].ElapsedSeconds) < inEndTime) { endPoint = activity.GPSRoute[activity.GPSRoute.Count - 1].Value; } else { // Set the end point ITimeValueEntry <IGPSPoint> ePoint = activity.GPSRoute.GetInterpolatedValue(inEndTime); if (ePoint != null) { endPoint = ePoint.Value; } } } // Create the GPSRoute for (int i = 0; i < activity.GPSRoute.Count; i++) { if (activity.GPSRoute.StartTime.AddSeconds(activity.GPSRoute[i].ElapsedSeconds) >= inStartTime && activity.GPSRoute.StartTime.AddSeconds(activity.GPSRoute[i].ElapsedSeconds) <= inEndTime) { recordGPS.Add(activity.GPSRoute.StartTime.AddSeconds(activity.GPSRoute[i].ElapsedSeconds), activity.GPSRoute[i].Value); } } } // Create the Distance Track INumericTimeDataSeries allDistanceTrack = ai.MovingDistanceMetersTrack; // Utilities.GetDistanceTrack(activity); INumericTimeDataSeries allElevationTrack = ai.SmoothedElevationTrack; // Utilities.GetElevationTrack(activity); // Work your way through the moving meters track to create all others if (allDistanceTrack != null) { for (int i = 0; i < allDistanceTrack.Count; i++) { DateTime time = allDistanceTrack.StartTime.AddSeconds(allDistanceTrack[i].ElapsedSeconds); if (time >= inStartTime && time <= inEndTime) { // Add the distance point distTrack.Add(time, allDistanceTrack[i].Value); ITimeValueEntry <float> point = null; // Find the elevation point at this time and add it if (allElevationTrack != null && allElevationTrack.Count > 0) { point = allElevationTrack.GetInterpolatedValue(time); if (point != null) { elevTrack.Add(time, point.Value); } } // Find the HR point at this time and add it if (activity.HeartRatePerMinuteTrack != null && activity.HeartRatePerMinuteTrack.Count > 0) { point = activity.HeartRatePerMinuteTrack.GetInterpolatedValue(time); if (point != null) { recordHRTrack.Add(time, point.Value); } } // Find the power point at this time and add it if (activity.PowerWattsTrack != null && activity.PowerWattsTrack.Count > 0) { point = activity.PowerWattsTrack.GetInterpolatedValue(time); if (point != null) { pwrTrack.Add(time, point.Value); } } // Find the cadence point at this time and add it if (activity.CadencePerMinuteTrack != null && activity.CadencePerMinuteTrack.Count > 0) { point = activity.CadencePerMinuteTrack.GetInterpolatedValue(time); if (point != null) { cadTrack.Add(time, point.Value); } } } else if (allDistanceTrack.StartTime.AddSeconds(allDistanceTrack[i].ElapsedSeconds) > inEndTime) { break; } } } // Get the start/end distance if (distTrack != null && distTrack.Count > 0) { startDistance = distTrack[0].Value; endDistance = distTrack[distTrack.Count - 1].Value; } else { startDistance = 0; endDistance = 0; } // Get the start/end elevation if (elevTrack != null && elevTrack.Count > 0) { startElevation = elevTrack[0].Value; endElevation = elevTrack[elevTrack.Count - 1].Value; } else { startElevation = 0; endElevation = 0; } // Build the record record = new Record(activity, category, recordGPS, recordHRTrack, pwrTrack, cadTrack, distTrack, elevTrack, inStartTime); // Create a reference id for this hill refId = Guid.NewGuid().ToString("D"); double distanceX = endDistance - startDistance; distance = distanceX; double elev = endElevation - startElevation; elevGain = elev; // Find the start percents from the distance track if (allDistanceTrack != null && allDistanceTrack.Count > 0) { startPercentDistance = startDistance / allDistanceTrack[allDistanceTrack.Count - 1].Value; endPercentDistance = endDistance / allDistanceTrack[allDistanceTrack.Count - 1].Value; startPercentTime = ((inStartTime - allDistanceTrack.StartTime).TotalSeconds / allDistanceTrack[allDistanceTrack.Count - 1].ElapsedSeconds); endPercentTime = ((inEndTime - allDistanceTrack.StartTime).TotalSeconds / allDistanceTrack[allDistanceTrack.Count - 1].ElapsedSeconds); } // Calculate the VAM (Velocity Ascended, Meters per hour) // Calculate the W/kg (Relative power) vam = 0; wKg = 0; if (elevGain > 0) { vam = (elevGain * 60f * 60f) / record.TotalTime.TotalSeconds; wKg = vam / ((2 + (avgGrade * 10f)) * 100f); } ActivityInfo aiRec = ActivityInfoCache.Instance.GetInfo(record.Activity); stoppedTime = aiRec.TimeNotMoving; }
protected void AddActivities(IImportResults importResults, IList <GlobalsatPacket.Train> trains, bool importSpeedTrackAsDistance, int detectPauses, int verbose) { foreach (GlobalsatPacket.Train train in trains) { DateTime pointTime = train.StartTime; IActivity activity = importResults.AddActivity(pointTime); activity.Metadata.Source = string.Format(CommonResources.Text.Devices.ImportJob_ActivityImportSource, sourceDescription); activity.TotalTimeEntered = train.TotalTime; activity.TotalDistanceMetersEntered = train.TotalDistanceMeters; activity.TotalCalories = train.TotalCalories; activity.MaximumHeartRatePerMinuteEntered = train.MaximumHeartRate; activity.AverageHeartRatePerMinuteEntered = train.AverageHeartRate; activity.MaximumCadencePerMinuteEntered = train.MaximumCadence; activity.AverageCadencePerMinuteEntered = train.AverageCadence; activity.MaximumPowerWattsEntered = train.MaximumPower; activity.AveragePowerWattsEntered = train.AveragePower; activity.TotalAscendMetersEntered = train.TotalAscend; activity.TotalDescendMetersEntered = -train.TotalDescend; activity.Notes += train.Comment; bool foundGPSPoint = false; bool foundCadencePoint = false; bool foundPowerPoint = false; activity.GPSRoute = new GPSRoute(); activity.HeartRatePerMinuteTrack = new NumericTimeDataSeries(); activity.CadencePerMinuteTrack = new NumericTimeDataSeries(); activity.PowerWattsTrack = new NumericTimeDataSeries(); activity.TemperatureCelsiusTrack = new NumericTimeDataSeries(); activity.ElevationMetersTrack = new NumericTimeDataSeries(); activity.DistanceMetersTrack = new DistanceDataTrack(); float pointDist = 0; double pointElapsed = 0; //As interval to first is not zero, add point activity.DistanceMetersTrack.Add(pointTime, pointDist); //Fix for (GB-580 only?) recording problem with interval 10 or larger (in fw before 2012-09) double?fixInterval = null; if (train.TrackPoints.Count > 1) { //All points except last has 0.1s interval double testIntervall = (train.TotalTime.TotalSeconds - train.TrackPoints[train.TrackPoints.Count - 1].IntervalTime) / (train.TrackPointCount - 1 - 1); if (testIntervall > 9.6) { fixInterval = testIntervall; } } DateTime pointTimePrev = System.DateTime.Now; DateTime pointTimeNext = System.DateTime.Now; bool insertPauseFirst = false; bool insertPauseLast = false; foreach (GhPacketBase.TrackPoint point in train.TrackPoints) { double time = point.IntervalTime; if (time < 0.11 && fixInterval != null) { time = (double)fixInterval; } pointElapsed += time; //Note: There is an intervall time to the first point, also if TGP says it is 0 pointTime = pointTime.AddSeconds(time); float dist = (float)(point.Speed * time); pointDist += dist; // TODO: How are GPS points indicated in indoor activities? //It seems like all are the same IGPSPoint gpsPoint = new GPSPoint((float)point.Latitude, (float)point.Longitude, point.Altitude); //There are no pause markers in the Globalsat protocol //Insert pauses when estimated/listed distance differs "too much" //Guess pauses - no info of real pause, but this can at least be marked in the track //Share setting with global split if (foundGPSPoint && activity.GPSRoute.Count > 0) { //estimated time for the pause double estimatedSec = 0; string info = ""; //speed & cadence method if (detectPauses == 2 && !float.IsNaN(point.Cadence)) { if (activity.GPSRoute.Count > 0) { float gpsDist = gpsPoint.DistanceMetersToPoint(activity.GPSRoute[activity.GPSRoute.Count - 1].Value); //Some limit on when to include pause //gpsDist must be higher than (all) GPS errors if (point.Cadence == 0 && point.Speed == 0 && gpsDist > 0 && insertPauseFirst == false) { insertPauseFirst = true; //We cannot know the true time for the expected pause, just set time between as pause to show on map estimatedSec = time; pointTimePrev = pointTime; } else if (point.Cadence != 0 && point.Speed != 0 && insertPauseFirst) { //last point insertPauseLast = true; pointTimeNext = pointTime; } } if (insertPauseLast) { insertPauseFirst = false; insertPauseLast = false; //Only add rounded pauses, ST only handles complete seconds activity.TimerPauses.Add(new ValueRange <DateTime>( pointTimePrev.AddMilliseconds(-pointTimePrev.Millisecond), pointTimeNext.AddMilliseconds(-pointTimeNext.Millisecond))); if (verbose >= 10) { //TODO: Remove remark when stable activity.Notes += string.Format("Added pause from {0} to {1} (dist:{2}, elapsedSec:{3} {4}) ", pointTimePrev.ToLocalTime(), pointTimeNext.ToLocalTime(), dist, pointTimeNext.Subtract(pointTimePrev).TotalSeconds, info) + System.Environment.NewLine; } } } else if (detectPauses == 1) { //distance method bool insertPause = false; //how far from the first point double perc = 0; //This code previously had code to detect pauses from HR too if (activity.GPSRoute.Count > 0) { float gpsDist = gpsPoint.DistanceMetersToPoint(activity.GPSRoute[activity.GPSRoute.Count - 1].Value); //Some limit on when to include pause //gpsDist must be higher than (all) GPS errors if (gpsDist > 100 && gpsDist > 3 * dist) { insertPause = true; //We cannot know the true time for the expected pause, just set time between as pause to show on map estimatedSec = time; if (gpsDist > 0) { perc = dist / gpsDist; } info += "gps: " + gpsDist; } } if (insertPause) { //Use complete seconds only - pause is estimated, ST handles sec internally and this must be synced to laps estimatedSec = Math.Round(estimatedSec); IGPSPoint newPoint = (new GPSPoint.ValueInterpolator()).Interpolate( activity.GPSRoute[activity.GPSRoute.Count - 1].Value, gpsPoint, perc); if (point == train.TrackPoints[train.TrackPoints.Count - 1]) { //Last point is incorrect, adjust (added normally) gpsPoint = newPoint; } else { if (estimatedSec <= time + 1) { pointTimePrev = pointTime.AddSeconds(-time); pointTimeNext = pointTime; } else { //Add extra point activity.DistanceMetersTrack.Add(pointTime, pointDist); if (point.Latitude != 0 || point.Longitude != 0) { activity.GPSRoute.Add(pointTime, newPoint); } else if (device.FitnessDevice.HasElevationTrack && !float.IsNaN(newPoint.ElevationMeters)) { activity.ElevationMetersTrack.Add(pointTime, newPoint.ElevationMeters); } pointTimePrev = pointTime; pointTimeNext = pointTime.AddSeconds((int)(estimatedSec - time)); pointTime = pointTimeNext; } //Only add rounded pauses, ST only handles complete seconds activity.TimerPauses.Add(new ValueRange <DateTime>( pointTimePrev.AddMilliseconds(-pointTimePrev.Millisecond + 1000), pointTimeNext.AddMilliseconds(-pointTimeNext.Millisecond - 9000))); if (verbose >= 10) { //TODO: Remove remark when stable activity.Notes += string.Format("Added pause from {0} to {1} (dist:{2}, elapsedSec:{3}, per:{4} {5}) ", pointTimePrev.ToLocalTime(), pointTimeNext.ToLocalTime(), dist, time, perc, info) + System.Environment.NewLine; } } } } } activity.DistanceMetersTrack.Add(pointTime, pointDist); //lat/lon is 0 if a device has never had a fix if (point.Latitude != 0 || point.Longitude != 0) { activity.GPSRoute.Add(pointTime, gpsPoint); //Check if lat/lon ever change (ignore altitude), GlobalSat reports last known location without a fix if (point.Latitude != train.TrackPoints[0].Latitude || point.Longitude != train.TrackPoints[0].Longitude) { foundGPSPoint = true; } } if (device.FitnessDevice.HasElevationTrack && !float.IsNaN(point.Altitude)) { activity.ElevationMetersTrack.Add(pointTime, point.Altitude); } //zero HR is invalid reading - drop if (point.HeartRate > 0) { activity.HeartRatePerMinuteTrack.Add(pointTime, point.HeartRate); } //Zero Cadence/Power may be valid values, if there are any values (no way to detect lost communication) activity.CadencePerMinuteTrack.Add(pointTime, point.Cadence); if (point.Cadence > 0) { foundCadencePoint = true; } activity.PowerWattsTrack.Add(pointTime, point.Power); if (point.Power > 0) { foundPowerPoint = true; } if (point.Temperature != 0x7fff) { activity.TemperatureCelsiusTrack.Add(pointTime, point.Temperature / 10.0F); } } TimeSpan lapElapsed = TimeSpan.Zero; int totalDistance = 0; foreach (GlobalsatPacket.Lap lapPacket in train.Laps) { DateTime lapTime = ZoneFiveSoftware.Common.Data.Algorithm.DateTimeRangeSeries.AddTimeAndPauses(activity.StartTime, lapElapsed, activity.TimerPauses); lapElapsed += lapPacket.LapTime; //Same as lapPacket.EndTime for unpaused ILapInfo lap = activity.Laps.Add(lapTime, lapPacket.LapTime); //Adding Distance would previously make ST fail to add new laps. The distance is needed only when there is no GPS (Markers added) if (activity.TotalDistanceMetersEntered > 0) { lap.TotalDistanceMeters = lapPacket.LapDistanceMeters; } if (lapPacket.LapCalories > 0) { lap.TotalCalories = lapPacket.LapCalories; } if (lapPacket.AverageHeartRate > 0) { lap.AverageHeartRatePerMinute = lapPacket.AverageHeartRate; } if (lapPacket.AverageCadence > 0) { lap.AverageCadencePerMinute = lapPacket.AverageCadence; } if (lapPacket.AveragePower > 0) { lap.AveragePowerWatts = lapPacket.AveragePower; } if (!foundGPSPoint && activity.ElevationMetersTrack != null && activity.ElevationMetersTrack.Count > 1) { //Limitation in ST: lap elevation not auto calc without GPS, lap preferred to elevation DateTime lapEnd = ZoneFiveSoftware.Common.Data.Algorithm.DateTimeRangeSeries.AddTimeAndPauses(activity.StartTime, lapElapsed, activity.TimerPauses); ITimeValueEntry <float> p1 = activity.ElevationMetersTrack.GetInterpolatedValue(lapTime); ITimeValueEntry <float> p2 = activity.ElevationMetersTrack.GetInterpolatedValue(lapEnd); if (p1 != null && p2 != null) { lap.ElevationChangeMeters = p2.Value - p1.Value; } } if (verbose >= 5) { //TODO: Localise outputs? lap.Notes = string.Format("MaxSpeed:{0:0.##}m/s MaxHr:{1} MinAlt:{2}m MaxAlt:{3}m", lapPacket.MaximumSpeed, lapPacket.MaximumHeartRate, lapPacket.MinimumAltitude, lapPacket.MaximumAltitude); //Not adding Power/Cadence - not available //lap.Notes = string.Format("MaxSpeed={0} MaxHr={1} MinAlt={2} MaxAlt={3} MaxCadence={4} MaxPower={5}", // lapPacket.MaximumSpeed, lapPacket.MaximumHeartRate, lapPacket.MinimumAltitude, lapPacket.MaximumAltitude, lapPacket.MaximumCadence, lapPacket.MaximumPower); } //Add distance markers from Globalsat. Will for sure be incorrect after pause insertion totalDistance += lapPacket.LapDistanceMeters; activity.DistanceMarkersMeters.Add(totalDistance); } if (!foundGPSPoint) { if (activity.GPSRoute.Count > 0) { activity.Notes += string.Format("No GPS. Last known latitude:{0}, longitude:{1}", activity.GPSRoute[0].Value.LatitudeDegrees, activity.GPSRoute[0].Value.LongitudeDegrees); } activity.GPSRoute = null; } //Keep elevation only if the device (may) record elevation separately from GPS //It may be used also if the user drops GPS if points have been recorded. //(ST may have partial use of elevation together with GPS on other parts in the future?) if (!device.FitnessDevice.HasElevationTrack || activity.ElevationMetersTrack.Count == 0) { activity.ElevationMetersTrack = null; } //Barometric devices occasionally have bad points last if (activity.ElevationMetersTrack != null && activity.ElevationMetersTrack.Count > 1 && Math.Abs(activity.ElevationMetersTrack[activity.ElevationMetersTrack.Count - 1].Value - activity.ElevationMetersTrack[activity.ElevationMetersTrack.Count - 2].Value) > 1) { if (activity.GPSRoute != null && activity.ElevationMetersTrack.StartTime.AddSeconds(activity.ElevationMetersTrack.TotalElapsedSeconds) == activity.GPSRoute.StartTime.AddSeconds(activity.GPSRoute.TotalElapsedSeconds)) { IGPSPoint g = activity.GPSRoute[activity.GPSRoute.Count - 1].Value; activity.GPSRoute.SetValueAt(activity.GPSRoute.Count - 1, new GPSPoint(g.LatitudeDegrees, g.LongitudeDegrees, float.NaN)); } activity.ElevationMetersTrack.RemoveAt(activity.ElevationMetersTrack.Count - 1); } if (activity.HeartRatePerMinuteTrack.Count == 0) { activity.HeartRatePerMinuteTrack = null; } if (!foundCadencePoint) { activity.CadencePerMinuteTrack = null; } if (!foundPowerPoint) { activity.PowerWattsTrack = null; } if (activity.TemperatureCelsiusTrack.Count == 0) { activity.TemperatureCelsiusTrack = null; } if (pointDist == 0 || !importSpeedTrackAsDistance && foundGPSPoint) { activity.DistanceMetersTrack = null; } #if DISTANCETRACK_FIX //attempt to fix import of distance track in 3.1.4515 - to be updated when rereleased //for now import distance track, to keep compatibility(?) try { activity.CalcSpeedFromDistanceTrack = importSpeedTrackAsDistance; } catch { //older than 3.1.4515, disable if (!importSpeedTrackAsDistance && !foundGPSPoint) { activity.DistanceMetersTrack = null; } } #endif } }
/// <summary> /// Create a grade track from an activity's elevation track /// </summary> /// <param name="activity">Activity that needs an grade track</param> /// <returns>A grade track created from the elevation track</returns> public static INumericTimeDataSeries GetGradeTrack(IActivity activity) { INumericTimeDataSeries gradeTrack = new NumericTimeDataSeries(); INumericTimeDataSeries elevationTrack = new NumericTimeDataSeries(); INumericTimeDataSeries distanceTrack = new NumericTimeDataSeries(); elevationTrack = GetElevationTrack(activity); distanceTrack = GetDistanceMovingTrack(activity); float grade = 0; float lastElevation = 0; float lastDistance = 0; float currentElevation = 0; float currentDistance = 0; DateTime startTime = activity.StartTime; if (gradeTrack != null && elevationTrack != null && distanceTrack != null) { for (int i = 0; i < elevationTrack.Count; i++) { if (i == 0) { gradeTrack.Add(startTime, grade); lastElevation = elevationTrack[i].Value; ITimeValueEntry <float> point = distanceTrack.GetInterpolatedValue(startTime.AddSeconds(elevationTrack[i].ElapsedSeconds)); if (point != null) { lastDistance = point.Value; } } else { ITimeValueEntry <float> point = distanceTrack.GetInterpolatedValue(startTime.AddSeconds(elevationTrack[i].ElapsedSeconds)); if (point != null) { currentDistance = point.Value; } else { currentDistance = lastDistance; } currentElevation = elevationTrack[i].Value; grade = (currentElevation - lastElevation) / (currentDistance - lastDistance); if (float.IsNaN(grade) || float.IsInfinity(grade)) { grade = 0; } gradeTrack.Add(startTime.AddSeconds(elevationTrack[i].ElapsedSeconds), grade); lastDistance = currentDistance; lastElevation = currentElevation; } } return(gradeTrack); } else { return(null); } }
internal static INumericTimeDataSeries STSmooth(INumericTimeDataSeries data, int seconds, out float min, out float max) { min = float.NaN; max = float.NaN; if (data != null) { if (data.Count == 0) { // Special case, no data return(new ZoneFiveSoftware.Common.Data.NumericTimeDataSeries()); } else if (data.Count == 1 || seconds < 1) { // Special case INumericTimeDataSeries copyData = new ZoneFiveSoftware.Common.Data.NumericTimeDataSeries(); min = data[0].Value; max = data[0].Value; foreach (ITimeValueEntry <float> entry in data) { copyData.Add(data.StartTime.AddSeconds(entry.ElapsedSeconds), entry.Value); min = Math.Min(min, entry.Value); max = Math.Max(max, entry.Value); } return(copyData); } min = float.MaxValue; max = float.MinValue; int smoothWidth = Math.Max(0, seconds * 2); // Total width/period. 'seconds' is the half-width... seconds on each side to smooth int denom = smoothWidth * 2; // Final value to divide by. It's divide by 2 because we're double-adding everything INumericTimeDataSeries smoothedData = new ZoneFiveSoftware.Common.Data.NumericTimeDataSeries(); // Loop through entire dataset for (int nEntry = 0; nEntry < data.Count; nEntry++) { ITimeValueEntry <float> entry = data[nEntry]; // TODO: Don't reset value & index markers, instead continue data here... double value = 0; double delta; // Data prior to entry long secondsRemaining = seconds; ITimeValueEntry <float> p1, p2; int increment = -1; int pos = nEntry - 1; p2 = data[nEntry]; while (secondsRemaining > 0 && pos >= 0) { p1 = data[pos]; if (SumValues(p2, p1, ref value, ref secondsRemaining)) { pos += increment; p2 = p1; } else { break; } } if (secondsRemaining > 0) { // Occurs at beginning of track when period extends before beginning of track. delta = data[0].Value * secondsRemaining * 2; value += delta; } // Data after entry secondsRemaining = seconds; increment = 1; pos = nEntry; p1 = data[nEntry]; while (secondsRemaining > 0 && pos < data.Count - 1) { p2 = data[pos + 1]; if (SumValues(p1, p2, ref value, ref secondsRemaining)) { // Move to next point pos += increment; p1 = p2; } else { break; } } if (secondsRemaining > 0) { // Occurs at end of track when period extends past end of track value += data[data.Count - 1].Value * secondsRemaining * 2; } float entryValue = (float)(value / denom); smoothedData.Add(data.StartTime.AddSeconds(entry.ElapsedSeconds), entryValue); min = Math.Min(min, entryValue); max = Math.Max(max, entryValue); // TODO: Remove 'first' p1 & p2 SumValues from 'value' if (data[nEntry].ElapsedSeconds - seconds < 0) { // Remove 1 second worth of first data point (multiply by 2 because everything is double here) value -= data[0].Value * 2; } else { // Remove data in middle of track (typical scenario) //value -= } } return(smoothedData); } else { return(null); } }