diff --git a/README.md b/README.md index 6584045..bd2e757 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ Trail running focused Garmin watch DataField (for myself) - distance - name +# FIT data + +- grade +- GAP +- vertical speed + # Settings - 3 theme colors diff --git a/manifest.xml b/manifest.xml index d5f72f2..d847040 100644 --- a/manifest.xml +++ b/manifest.xml @@ -1,7 +1,7 @@ - + diff --git a/resources/fitcontributions/fitcontributions.xml b/resources/fitcontributions/fitcontributions.xml index 4c0d1e2..6879595 100644 --- a/resources/fitcontributions/fitcontributions.xml +++ b/resources/fitcontributions/fitcontributions.xml @@ -1,4 +1,5 @@ + + + + + + + + + - + + + + diff --git a/resources/settings/properties.xml b/resources/settings/properties.xml index 451636c..c4adf63 100644 --- a/resources/settings/properties.xml +++ b/resources/settings/properties.xml @@ -6,6 +6,6 @@ 0 false true - true + false 1 diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index 49a1758..152874e 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -22,4 +22,11 @@ Grade Grade Adjusted Pace Vertical Speed + Grade (average) + Grade (min) + Grade (max) + Grade Adjusted Pace (average) + Vertical Speed (average) + Vertical Speed (min) + Vertical Speed (max) diff --git a/source/RepaFieldTrack.mc b/source/RepaFieldTrack.mc index 91d6744..368bca7 100644 --- a/source/RepaFieldTrack.mc +++ b/source/RepaFieldTrack.mc @@ -58,8 +58,9 @@ class Track extends WatchUi.Drawable { var h = dc.getHeight(); var astart = 150; var aend = 390; + var offtrack = _offCourse > 50.0f; dc.setPenWidth((dc.getWidth() * 0.01).toNumber()); - dc.setColor(0x555555, Graphics.COLOR_TRANSPARENT); + dc.setColor(offtrack ? 0x880000 : 0x555555, Graphics.COLOR_TRANSPARENT); dc.drawArc(w / 2, h / 2, w / 2 - 2, Graphics.ARC_COUNTER_CLOCKWISE, astart, aend); if (trackPercentage <= 0.0f) { @@ -67,7 +68,7 @@ class Track extends WatchUi.Drawable { } // color - if (_offCourse > 50.0f) { + if (offtrack) { dc.setColor(0xFF0000, Graphics.COLOR_TRANSPARENT); } else if (trackPercentage < 0.2) { dc.setColor(0x8800FF, Graphics.COLOR_TRANSPARENT); @@ -100,7 +101,7 @@ class Track extends WatchUi.Drawable { anext = aend; } dc.setPenWidth((dc.getWidth() * 0.01).toNumber()); - dc.setColor(0xFFFF00, Graphics.COLOR_TRANSPARENT); + dc.setColor(offtrack ? 0xAA0000 : 0xAAAAAA, Graphics.COLOR_TRANSPARENT); dc.drawArc(w / 2, h / 2, w / 2 - 2, Graphics.ARC_COUNTER_CLOCKWISE, acurrent, anext); // next point name diff --git a/source/RepaFieldView.mc b/source/RepaFieldView.mc index f4a0770..c1a0c87 100644 --- a/source/RepaFieldView.mc +++ b/source/RepaFieldView.mc @@ -16,8 +16,18 @@ const TLF_GAP = 2; const TLF_VSPEED = 3; const FIT_GRADE_ID = 0; +const FIT_GRADE_SUM_MIN_ID = 5; +const FIT_GRADE_SUM_MAX_ID = 6; +const FIT_GRADE_SUM_AVG_ID = 7; +const FIT_GRADE_LAP_AVG_ID = 8; const FIT_GAP_ID = 1; +const FIT_GAP_SUM_AVG_ID = 3; +const FIT_GAP_LAP_AVG_ID = 4; const FIT_VSPEED_ID = 2; +const FIT_VSPEED_SUM_MIN_ID = 9; +const FIT_VSPEED_SUM_MAX_ID = 10; +const FIT_VSPEED_SUM_AVG_ID = 11; +const FIT_VSPEED_LAP_AVG_ID = 12; function displayHr(hr as Number, type as Number, zones as Array) as String { if (hr == 0) { @@ -86,10 +96,15 @@ class RepaFieldView extends WatchUi.DataField { // fit hidden var fitGrade; + hidden var fitGradeSumAvg; + hidden var fitGradeLapAvg; hidden var fitGAP; hidden var fitVSpeed; + hidden var fitVSpeedSumAvg; + hidden var fitVSpeedLapAvg; // values + hidden var timerRunning as Boolean = false; hidden var hrTicks as Number; hidden var hrValue as Numeric; hidden var ahrValue as Numeric; @@ -110,10 +125,10 @@ class RepaFieldView extends WatchUi.DataField { hidden var edrop as Number; hidden var cadence as Number; hidden var grade as RollingAverage; - hidden var cgrade as Float; + hidden var cgrade; hidden var vspeed as RollingAverage; - hidden var cvspeed as Float; - hidden var gap as Float; + hidden var cvspeed; + hidden var cgap; function initialize() { DataField.initialize(); @@ -155,10 +170,10 @@ class RepaFieldView extends WatchUi.DataField { edrop = 0; cadence = 0; grade = new RollingAverage(10); - cgrade = 0.0f; + cgrade = null; vspeed = new RollingAverage(10); - cvspeed = 0.0f; - gap = 0.0f; + cvspeed = null; + cgap = null; var settings = System.getDeviceSettings(); isDistanceMetric = settings.distanceUnits == System.UNIT_METRIC; @@ -166,9 +181,14 @@ class RepaFieldView extends WatchUi.DataField { isPaceMetric = settings.paceUnits == System.UNIT_METRIC; // fit fields + // TODO: refator into separate function/class fitGrade = null; + fitGradeSumAvg = null; + fitGradeLapAvg = null; fitGAP = null; fitVSpeed = null; + fitVSpeedSumAvg = null; + fitVSpeedLapAvg = null; if (Application.Properties.getValue("saveToFit")) { fitGrade = DataField.createField( "grade", @@ -179,6 +199,24 @@ class RepaFieldView extends WatchUi.DataField { :units=>"%", } ); + fitGradeSumAvg = DataField.createField( + "avg_grade", + FIT_GRADE_SUM_AVG_ID, + FitContributor.DATA_TYPE_FLOAT, + { + :mesgType=>FitContributor.MESG_TYPE_RECORD, + :units=>"%", + } + ); + fitGradeLapAvg = DataField.createField( + "avg_grade", + FIT_GRADE_LAP_AVG_ID, + FitContributor.DATA_TYPE_FLOAT, + { + :mesgType=>FitContributor.MESG_TYPE_RECORD, + :units=>"%", + } + ); fitGAP = DataField.createField( "gap", FIT_GAP_ID, @@ -197,9 +235,58 @@ class RepaFieldView extends WatchUi.DataField { :units=>isElevationMetric ? "m/min" : "ft/min", } ); + fitVSpeedSumAvg = DataField.createField( + "avg_vspeed", + FIT_VSPEED_SUM_AVG_ID, + FitContributor.DATA_TYPE_FLOAT, + { + :mesgType=>FitContributor.MESG_TYPE_RECORD, + :units=>isElevationMetric ? "m/min" : "ft/min", + } + ); + fitVSpeedLapAvg = DataField.createField( + "avg_vspeed", + FIT_VSPEED_LAP_AVG_ID, + FitContributor.DATA_TYPE_FLOAT, + { + :mesgType=>FitContributor.MESG_TYPE_RECORD, + :units=>isElevationMetric ? "m/min" : "ft/min", + } + ); } } + public function onNextMultisportLeg() as Void { + grade.reset(); + vspeed.reset(); + } + + public function onTimerLap() as Void { + grade.lapReset(); + vspeed.lapReset(); + } + + public function onTimerReset() as Void { + grade.reset(); + vspeed.reset(); + } + + public function onTimerStart() as Void { + timerRunning = true; + } + + public function onTimerResume() as Void { + timerRunning = true; + } + + public function onTimerPause() as Void { + timerRunning = false; + } + + public function onTimerStop() as Void { + timerRunning = false; + } + function tickHr(v as Number) { hrTicks++; var hrzsize = hrZones.size(); @@ -295,7 +382,7 @@ class RepaFieldView extends WatchUi.DataField { function compute(info as Activity.Info) as Void { // update rolling values before updating normal fields // only calculate them when some time has passed - if (info.timerTime != null && info.timerTime > 0) { + if (info.timerTime != null && info.timerTime > 0 && timerRunning) { if (info.altitude != null) { var altChange = info.altitude - altitude; @@ -305,7 +392,10 @@ class RepaFieldView extends WatchUi.DataField { if (distChange > 0) { grade.insert(altChange / distChange); } - cgrade = grade.get() * 100; + var currentGrade = grade.getRolling(); + if (currentGrade) { + cgrade = currentGrade * 100; + } } // vspeed - m/min or ft/min @@ -316,7 +406,10 @@ class RepaFieldView extends WatchUi.DataField { } else { vspeed.insert(altChange / (timerChange / 60000.0)); } - cvspeed = vspeed.get(); + var currentVSpeed = vspeed.getRolling(); + if (currentVSpeed != null ) { + cvspeed = currentVSpeed; + } } } } @@ -415,7 +508,9 @@ class RepaFieldView extends WatchUi.DataField { cadence = 0; } - gap = adjustPaceForGrade(pace, cgrade / 100); + if (cgrade != null) { + cgap = adjustPaceForGrade(pace, cgrade / 100); + } // convert units to imperial if needed if (!isDistanceMetric) { @@ -435,17 +530,30 @@ class RepaFieldView extends WatchUi.DataField { } // fit update - if (fitGrade != null) { - fitGrade.setData(cgrade); - } - if (fitGAP != null) { - fitGAP.setData(gap); - } - if (fitVSpeed != null) { - fitVSpeed.setData(cvspeed); + // TODO: refactor into separate function/class + if (timerRunning) { + if (fitGrade != null) { + fitGrade.setData(cgrade ? cgrade : 0); + var gradeSumAvg = grade.totalAvg(); + fitGradeSumAvg.setData(gradeSumAvg ? gradeSumAvg : 0); + var gradeLapAvg = grade.lapAvg(); + fitGradeLapAvg.setData(gradeLapAvg ? gradeLapAvg : 0); + } + if (fitGAP != null) { + fitGAP.setData(cgap ? cgap : 0); + } + if (fitVSpeed != null) { + fitVSpeed.setData(cvspeed ? cvspeed : 0); + var vsSumAvg = grade.totalAvg(); + fitVSpeedSumAvg.setData(vsSumAvg ? vsSumAvg : 0); + var vsLapAvg = grade.lapAvg(); + fitVSpeedLapAvg.setData(vsLapAvg ? vsLapAvg : 0); + } } } + // TODO: onlap - reset lap metrics + // Display the value you computed here. This will be called // once a second when the data field is visible. function onUpdate(dc as Dc) as Void { @@ -559,30 +667,38 @@ class RepaFieldView extends WatchUi.DataField { // TLF if (fCadence != null) { if (tlFieldData == TLF_GRADE) { - var gradeColor = calculateZoneColor(cgrade, gradeZones, gradeZoneColors); - fCadence.setColor(gradeColor); - if (cgrade >= 10 || cgrade <= -10) { - fCadence.setText(cgrade.format("%.0f")); + if (cgrade != null) { + var gradeColor = calculateZoneColor(cgrade, gradeZones, gradeZoneColors); + fCadence.setColor(gradeColor); + if (cgrade >= 10 || cgrade <= -10) { + fCadence.setText(cgrade.format("%.0f")); + } else { + fCadence.setText(cgrade.format("%.1f")); + } } else { - fCadence.setText(cgrade.format("%.1f")); + fCadence.setText("-"); } } else if (tlFieldData == TLF_GAP) { fCadence.setColor(themeColor2); - if (pace != 0) { + if (pace != 0 && cgap != null) { // TODO color - var gapmin = gap.toNumber(); - var gapsec = (gap - gapmin) * 60; + var gapmin = cgap.toNumber(); + var gapsec = (cgap - gapmin) * 60; fCadence.setText(gapmin.format("%d") + ":" + gapsec.format("%02d")); } else { fCadence.setText("-"); } } else if (tlFieldData == TLF_VSPEED) { - var vsColor = calculateZoneColor(cvspeed, vsZones, vsZoneColors); - fCadence.setColor(vsColor); - if (cvspeed >= 10 || cvspeed <= -10) { - fCadence.setText(cvspeed.format("%.0f")); + if (cvspeed != null) { + var vsColor = calculateZoneColor(cvspeed, vsZones, vsZoneColors); + fCadence.setColor(vsColor); + if (cvspeed >= 10 || cvspeed <= -10) { + fCadence.setText(cvspeed.format("%.0f")); + } else { + fCadence.setText(cvspeed.format("%.1f")); + } } else { - fCadence.setText(cvspeed.format("%.1f")); + fCadence.setText("-"); } } else { var cadenceColor = calculateZoneColor(cadence, cadenceZones, cadenceZoneColors); diff --git a/source/RollingAverage.mc b/source/RollingAverage.mc index 6b3114d..f5baa7e 100644 --- a/source/RollingAverage.mc +++ b/source/RollingAverage.mc @@ -5,21 +5,94 @@ import Toybox.System; class RollingAverage { hidden var _size as Number; - hidden var _values as Array; + hidden var _safeCount as Number; + hidden var _values as Array; hidden var _index as Number; + hidden var _last; + hidden var _totalSum as Float; + hidden var _totalCount as Number; + hidden var _totalMin; + hidden var _totalMax; + hidden var _lapSum; + hidden var _lapCount as Number; + hidden var _lapMin; + hidden var _lapMax; function initialize(size as Number) { _size = size; + _safeCount = size * 2; _values = new[size]; _index = 0; + _last = null; + + _totalSum = 0.0; + _totalCount = 0; + _totalMin = null; + _totalMax = null; + + _lapSum = 0.0; + _lapCount = 0; + _lapMin = null; + _lapMax = null; + + } + + function setSafeCount(count as Number) { + _safeCount = count; } function insert(value as Numeric) { + // total + _totalCount += 1; + _totalSum += value; + if (_totalMin == null || value < _totalMin) { + _totalMin = value; + } + if (_totalMax == null || value > _totalMax) { + _totalMax = value; + } + + // lap + _lapCount += 1; + _lapSum += value; + if (_lapMin == null || value < _lapMin) { + _lapMin = value; + } + if (_lapMax == null || value > _lapMax) { + _lapMax = value; + } + + // rolling _values[_index] = value; _index = (_index + 1) % _size; } - function get() as Numeric { + function totalAvg() { return _totalCount == 0 ? null : _totalSum / _totalCount; } + function totalMin() { return _totalMin; } + function totalMax() { return _totalMax; } + function lapAvg() { return _lapCount == 0 ? null : _lapSum / _lapCount; } + function lapMin() { return _lapMin; } + function lapMax() { return _lapMax; } + + function reset() { + _totalSum = 0.0; + _totalCount = 0; + _totalMin = null; + _totalMax = null; + lapReset(); + } + + function lapReset() { + _lapSum = 0.0; + _lapCount = 0; + _lapMin = null; + _lapMax = null; + } + + function getRolling() { + if (_totalCount < _safeCount) { + return null; + } var sum = 0.0; var count = 0; for (var i = 0; i < _size; i++) {