/// <summary> /// Gets a data track that represents the corresponding data value for each maximum effort. /// Example (single data point): 300 watts for 10 minutes: What was the avg cadence over this max. effort? HR, etc. /// This data series will return the track to answer this question for every point in the max effort. /// </summary> /// <param name="activity">Activity to analyze.</param> /// <param name="pointType">Cadence, power, hr, etc.</param> /// <param name="timeTrack">Coded data track describing when the max efforts occurred. /// Time track aligns with the max effort period (same as normal mean max time scale) /// Values represent max effort start time. /// Start time + Elapsed seconds = end time of max effort</param> /// <returns></returns> internal static INumericTimeDataSeries GetAvgTrack(IActivity activity, Common.TrackType pointType, INumericTimeDataSeries timeTrack) { if (activity == null || timeTrack == null || timeTrack.Count == 0) { return(null); } INumericTimeDataSeries track = new NumericTimeDataSeries(); foreach (TimeValueEntry <float> point in timeTrack) { // Start time is relative to activity start time. // Some data tracks don't start at same time as activity // point.Elapsed seconds is the period // point.Value is the start time relative to activity start float value = GetAvgValue(activity, pointType, activity.StartTime.AddSeconds(point.Value), point.ElapsedSeconds); if (!float.IsNaN(value)) { track.Add(timeTrack.EntryDateTime(point), value); } else if (point.ElapsedSeconds == 0) { // This is to address the first point where the period = 0 value = GetAvgValue(activity, pointType, activity.StartTime, 1); track.Add(timeTrack.StartTime, value); } } return(track); }
/// <summary> /// Change ChartType (HR, Power, Cadence, etc.) from menu /// </summary> /// <param name="sender">menu item that was clicked</param> /// <param name="e">This item is not used</param> private void bannerMenuItem_Click(object sender, EventArgs e) { ToolStripMenuItem selected = sender as ToolStripMenuItem; for (int i = 0; i < mnuDetail.Items.Count; i++) { ToolStripMenuItem item = mnuDetail.Items[i] as ToolStripMenuItem; if (item != null) { if (item != selected) { item.Checked = false; } else { item.Checked = true; } } else { // ToolStrip Separator encountered. Stop evaluating break; } } bnrReport.Text = selected.Text.ToString(); ChartType = (Common.TrackType)Enum.Parse(typeof(Common.TrackType), selected.Tag.ToString()); }
internal static void InitializeSettings() { chartLines = null; primaryChart = Common.TrackType.Power; reportChart = Common.TrackType.Power; criticalPowerLines = null; }
/// <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> /// Gets the cached track if available, or calculates from scratch if not available. /// </summary> /// <param name="activity">Activity to calculate</param> /// <param name="chartType">Which track to calculate</param> /// <param name="create">Cache the track if it doesn't exist. /// True will always return a track. /// False will return the cached track, or null if not already cached.</param> /// <returns>Cached track if available, or empty track if not found.</returns> private static INumericTimeDataSeries GetMeanMaxTrack(IActivity activity, Common.TrackType chartType, out INumericTimeDataSeries timeTrack, bool create) { if (activity == null) { timeTrack = new NumericTimeDataSeries(); return(new NumericTimeDataSeries()); } string id = activity.ReferenceId + chartType; if (tracks.ContainsKey(id)) { // Returned cached value from memory :) timeTrack = tracks[id + "T"]; return(tracks[id]); } else if (create) { // Not in cache, create a new mean-max track :( INumericTimeDataSeries track = new NumericTimeDataSeries(); timeTrack = null; switch (chartType) { case Common.TrackType.HR: { track = GetMeanMaxTrack(activity.HeartRatePerMinuteTrack, out timeTrack, activity.StartTime); break; } case Common.TrackType.Power: { track = GetMeanMaxTrack(activity.PowerWattsTrack, out timeTrack, activity.StartTime); break; } case Common.TrackType.Cadence: { track = GetMeanMaxTrack(activity.CadencePerMinuteTrack, out timeTrack, activity.StartTime); break; } } // Add data track and related 'T'ime track to cache for next time MeanMaxCache.AddTrack(track, id); MeanMaxCache.AddTrack(timeTrack, id + "T"); return(track); } else { // Not previously cached, AND requested not to create a new cached item. timeTrack = new NumericTimeDataSeries(); return(null); } }
/// <summary> /// Add 'track' to 'graph' and apply labels based on 'chartType' /// </summary> /// <param name="track">Data track</param> /// <param name="graph">Which graph to stick the data on</param> /// <param name="chartType">This determines the labeling, coloring, etc. (all appearance related)</param> internal static void updateZedGraph(Dictionary <CriticalLineDefinition, INumericTimeDataSeries> tracks, ZedGraphControl graph, Common.TrackType chartType) { GraphPane myPane = graph.GraphPane; myPane.XAxis.Title.Text = CommonResources.Text.LabelDate; myPane.XAxis.Type = AxisType.Date; Color mainCurveColor = Common.GetColor(chartType); string tag = string.Empty; switch (chartType) { case Common.TrackType.Cadence: myPane.YAxis.Title.Text = CommonResources.Text.LabelCadence + " (" + CommonResources.Text.LabelRPM + ")"; tag = ColumnDefinition.cadenceID; break; case Common.TrackType.HR: myPane.YAxis.Title.Text = CommonResources.Text.LabelHeartRate + " (" + CommonResources.Text.LabelBPM + ")"; tag = ColumnDefinition.hrID; break; case Common.TrackType.Power: myPane.YAxis.Title.Text = CommonResources.Text.LabelPower + " (" + CommonResources.Text.LabelWatts + ")"; tag = ColumnDefinition.powerID; break; } LineItem curve; myPane.CurveList.Clear(); myPane.YAxis.Title.FontSpec.FontColor = mainCurveColor; myPane.YAxis.Scale.FontSpec.FontColor = mainCurveColor; // Add each critical values chart foreach (KeyValuePair <CriticalLineDefinition, INumericTimeDataSeries> track in tracks) { PointPairList zedTrack = new PointPairList(); DateTime itemDate; // Assemble associated track data foreach (ITimeValueEntry <float> item in track.Value) { itemDate = track.Value.EntryDateTime(item); zedTrack.Add(new XDate(itemDate), item.Value); } // Setup display properties of associated track Color color = track.Key.LineColor; curve = myPane.AddCurve(track.Key.Name, zedTrack, color, SymbolType.None); curve.Line.Width = 1f; curve.Line.Fill.Type = FillType.None; curve.Line.IsAntiAlias = true; //curve.Line.IsSmooth = true; //curve.Line.SmoothTension = .15f; curve.Tag = track.Key.ReferenceId; } if (tracks.Count > 0) { graph.AxisChange(); } graph.Refresh(); }
/// <summary> /// Get critical value track. This is the full track of critical values for various activities over a date range until 'now'. /// /// </summary> /// <param name="activities">The list of activities to consider</param> /// <param name="trackType">Which data track to evaluate</param> /// <param name="seconds">The critical period, measured in seconds. Example, for 5 minute power this would be 300.</param> /// <returns>Returns a data track of values over a date range. 1 value per day.</returns> internal static INumericTimeDataSeries GetCriticalTrack(IEnumerable <IActivity> activities, Common.TrackType trackType, float seconds) { float criticalValue = 0; SortedList <DateTime, float> criticalData = new SortedList <DateTime, float>(); INumericTimeDataSeries criticalTrack = new NumericTimeDataSeries(); float i = 0, count = (activities as List <IActivity>).Count; // Populate activity data foreach (IActivity activity in activities) { Progress = (int)(i++ / count * 100f); Application.DoEvents(); // Filter bad data if (activity.StartTime.Year != 1) { criticalValue = MeanMaxCache.GetCriticalValue(activity, trackType, seconds); if (!float.IsNaN(criticalValue)) { DateTime activityDate = activity.StartTime.Add(activity.TimeZoneUtcOffset).Date; if (criticalData.ContainsKey(activityDate)) { // Handle days with multiple activities // Update with higher value criticalData[activityDate] = Math.Max(criticalValue, criticalData[activityDate]); } else if (criticalValue != 0) { // Add critical value to data set criticalData.Add(activityDate, criticalValue); } } } } // Construct critical data track DateTime firstDate; if (criticalData.Count > 0) { firstDate = criticalData.Keys[0]; criticalValue = criticalData[firstDate]; } else { firstDate = DateTime.Now; } float value; for (DateTime day = firstDate; day < DateTime.Now;) { // Decay the critical value first, value for 'today' //criticalValue = criticalValue - criticalValue / GlobalSettings.Instance.TCc; if (criticalData.TryGetValue(day.Date, out value)) { // TODO: Determine best way to incorporate decay values // Value recorded for today or previous (decayed) value //criticalValue = Math.Max(value, criticalValue); //criticalTrack.Add(day, criticalValue); criticalTrack.Add(day, value); } else { // Decayed value //criticalTrack.Add(day, criticalValue); } // Next day day = day.AddDays(1); } Progress = -1; return(criticalTrack); }
/// <summary> /// Gets the cached track if available, or calculates from scratch if not available. /// </summary> /// <param name="activity">Activity to calculate</param> /// <param name="chartType">Which track to calculate</param> /// <returns>Cached track if available, or empty track if not found.</returns> internal static INumericTimeDataSeries GetMeanMaxTrack(IActivity activity, Common.TrackType chartType, out INumericTimeDataSeries timeTrack) { return(GetMeanMaxTrack(activity, chartType, out timeTrack, true)); }
/// <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); } }
/// <summary> /// Add 'track' to 'graph' and apply labels based on 'chartType' /// </summary> /// <param name="track">Data track</param> /// <param name="graph">Which graph to stick the data on</param> /// <param name="chartType">This determines the labeling, coloring, etc. (all appearance related)</param> internal static void updateZedGraph(INumericTimeDataSeries track, Dictionary <string, INumericTimeDataSeries> assocTracks, ZedGraphControl graph, Common.TrackType chartType) { GraphPane myPane = graph.GraphPane; myPane.XAxis.Title.Text = CommonResources.Text.LabelTime; myPane.XAxis.Type = AxisType.Log; Color mainCurveColor = Common.GetColor(chartType); string tag = string.Empty; switch (chartType) { case Common.TrackType.Cadence: myPane.YAxis.Title.Text = CommonResources.Text.LabelCadence + " (" + CommonResources.Text.LabelRPM + ")"; tag = ColumnDefinition.cadenceID; break; case Common.TrackType.HR: myPane.YAxis.Title.Text = CommonResources.Text.LabelHeartRate + " (" + CommonResources.Text.LabelBPM + ")"; tag = ColumnDefinition.hrID; break; case Common.TrackType.Power: myPane.YAxis.Title.Text = CommonResources.Text.LabelPower + " (" + CommonResources.Text.LabelWatts + ")"; tag = ColumnDefinition.powerID; break; } myPane.XAxis.MinorTic.IsOutside = true; // Add primary mean max chart PointPairList zedTrack = new PointPairList(); INumericTimeDataSeries timeTrack = null; if (assocTracks.ContainsKey(timeId)) { timeTrack = assocTracks[timeId]; } foreach (ITimeValueEntry <float> item in track) { float time = -1; if (timeTrack != null) { int index = track.IndexOf(item); time = timeTrack[index].Value; } zedTrack.Add(item.ElapsedSeconds, item.Value, time); } myPane.CurveList.Clear(); LineItem curve = myPane.AddCurve("Curve Label", zedTrack, mainCurveColor, SymbolType.None); curve.Line.Width = 1f; curve.Line.Fill.Type = FillType.Solid; curve.Line.Fill.Color = Color.FromArgb(50, mainCurveColor); curve.Tag = tag; curve.Line.IsAntiAlias = true; myPane.YAxis.Title.FontSpec.FontColor = mainCurveColor; myPane.YAxis.Scale.FontSpec.FontColor = mainCurveColor; // Add secondary correlation charts int yIndex = 1; foreach (string id in GlobalSettings.Instance.ChartLines) { if (assocTracks.ContainsKey(id)) { zedTrack = new PointPairList(); // Assemble associated track data foreach (ITimeValueEntry <float> item in assocTracks[id]) { // Include time of occurrance float time = -1; int index = assocTracks[id].IndexOf(item); if (timeTrack != null && timeTrack.Count > index) { time = timeTrack[index].Value; } zedTrack.Add(item.ElapsedSeconds, item.Value, time); } // Setup display properties of associated track Color color = ColumnDefinition.GetTrackColor(id); curve = myPane.AddCurve(id, zedTrack, color, SymbolType.None); curve.Line.Width = 1f; curve.Line.Fill.Type = FillType.None; curve.Line.IsAntiAlias = true; curve.Tag = id; curve.IsY2Axis = true; yIndex = myPane.Y2AxisList.IndexOfTag(id); if (yIndex != -1) { // Set to existing axis curve.YAxisIndex = yIndex; } else { // Oops... ERROR! } } } if (track.Count > 0) { graph.AxisChange(); } graph.Refresh(); }
internal void RefreshPage() { if (activities == null) { return; } INumericTimeDataSeries meanMax = new NumericTimeDataSeries(); INumericTimeDataSeries timeTrack = new NumericTimeDataSeries(); INumericTimeDataSeries activityMax = new NumericTimeDataSeries(); Dictionary <string, INumericTimeDataSeries> assocTracks = new Dictionary <string, INumericTimeDataSeries>(); SortedList <float, float> mmTempList = new SortedList <float, float>(); meanMax.AllowMultipleAtSameTime = true; DateTime start = DateTime.Now.Date; foreach (IActivity mmact in activities) { // Try to pull from memory if available activityMax = MeanMaxCache.GetMeanMaxTrack(mmact, ChartType, out timeTrack); #if DebugOFF Utilities.ExportTrack(activityMax, "C:\\STexports\\" + "MeanMax" + ".csv"); Utilities.ExportTrack(mmact.PowerWattsTrack, "C:\\STexports\\" + "RawPowerTrack" + ".csv"); Utilities.ExportTrack(mmact.HeartRatePerMinuteTrack, "C:\\STexports\\" + "RawHeartRate" + ".csv"); Utilities.ExportTrack(mmact.CadencePerMinuteTrack, "C:\\STexports\\" + "RawCadence" + ".csv"); #endif int numActivities = (activities as List <IActivity>).Count; if (numActivities == 1) { // Add timetrack - Note this this only available for single activity analysis assocTracks.Add(timeId, timeTrack); // Add associated chart lines (matching HR, Cadence, etc.) foreach (string id in GlobalSettings.Instance.ChartLines) { Common.TrackType lineType = ColumnDefinition.GetTrackType(id); if (lineType != ChartType) { // Get associated track (HR, Cad, etc.) // This line is similar to the Mean-max chart where the time track is the 'period' // but the data is the HR, Cad., etc. that occurred at the -same time- as the Max effort occurred // rather than being a max value in itself. // The value is the average value during the range of time as opposed to a single entry INumericTimeDataSeries track = MeanMaxCache.GetAvgTrack(mmact, lineType, timeTrack); #if DebugOFF Utilities.ExportTrack(track, "C:\\STexports\\MM" + ColumnDefinition.GetText(id) + ".csv"); #endif if (!assocTracks.ContainsKey(id) && track != null) { // Store associated tracks for charting assocTracks.Add(id, track); } else { // TODO: Combine multiple tracks for averaging. Currently it'll only display the first activity track :( } } } } // Compile all points together (used for multiple activities) foreach (TimeValueEntry <float> item in activityMax) { /* If(entry not exist in meanMax || current value > existing value) * { * Add new MM entry * Update assoc track values * } * else * { * * } */ // Add to temporary sortedlist if (!mmTempList.ContainsKey(item.ElapsedSeconds)) { mmTempList.Add(item.ElapsedSeconds, item.Value); } else if (mmTempList.ContainsKey(item.ElapsedSeconds) && mmTempList[item.ElapsedSeconds] < item.Value) { mmTempList[item.ElapsedSeconds] = item.Value; } //meanMax.Add(start.AddSeconds(item.ElapsedSeconds), item.Value); } // Copy sorted temporary list to proper numeric time series foreach (float seconds in mmTempList.Keys) { meanMax.Add(start.AddSeconds(seconds), mmTempList[seconds]); } // Remove low points (used for multiple activities) for (int i = meanMax.Count - 2; i >= 0; i--) { if (meanMax[i].Value < meanMax[i + 1].Value) { meanMax.RemoveAt(i); } } } updateZedGraph(meanMax, assocTracks, zedChart, ChartType); }