running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

commit 13109144ed96fcf0c47b92e8e9690a1788be37de
parent a773b6b3b949406be28caf7d531254d97ef85965
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 12 Sep 2021 14:39:38 -0700

Add advanced options in race calculator

Diffstat:
Msrc/assets/global.css | 23+++++++++++++++++++++++
Msrc/views/RaceCalculator.vue | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mtests/unit/views/RaceCalculator.spec.js | 4++--
3 files changed, 191 insertions(+), 10 deletions(-)

diff --git a/src/assets/global.css b/src/assets/global.css @@ -15,6 +15,21 @@ input, select, button { button { cursor: pointer; } +.link, .link:focus, .link:active, .link:hover { + border: none; + background: none; +} +a, .link { + text-decoration: none; +} +a:focus, .link:focus { + text-decoration: underline; +} +@media (hover: hover) { + a:hover, .link:hover { + text-decoration: underline; + } +} /* styles for icons */ .icon { @@ -61,6 +76,9 @@ button:active { button, input, select, tr { border: 1px solid var(--background5); } +a, .link { + color: var(--link); +} /* light/default theme */ :root { @@ -84,6 +102,9 @@ button, input, select, tr { /* The foreground color of app elements */ --foreground: #000000; + + /* The color of links */ + --link: hsl(210, 100%, 40%); } /* dark mode */ @@ -95,6 +116,7 @@ button, input, select, tr { --background4: hsl(210, 20%, 25%); --background5: hsl(210, 20%, 30%); --foreground: #e8e8e8; + --link: hsl(210, 100%, 65%); } .icon img { filter: invert(90%); @@ -110,5 +132,6 @@ button, input, select, tr { --background4: #ffffff; --background5: #000000; --foreground: #000000; + --link: #0000ff; } } diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -17,15 +17,51 @@ </div> </div> + <h2> + Advanced + <button class="link" @click="showAdvancedOptions=!showAdvancedOptions"> + {{ showAdvancedOptions ? '[hide]' : '[show]' }} + </button> + </h2> + <div class="advanced-options" v-show="showAdvancedOptions"> + <div> + Prediction Model: + <select v-model="model" aria-label="Prediction Model"> + <option value="AverageModel">Average</option> + <option value="PurdyPointsModel">Purdy Points Model</option> + <option value="VO2MaxModel">V&#775;O&#8322; Max Model</option> + <option value="CameronModel">Cameron's Model</option> + <option value="RiegelModel">Riegel's Model</option> + </select> + </div> + <div> + Riegel Exponent: + <decimal-input v-model="riegelExponent" aria-label="Riegel Exponent" :min="1" :max="1.3" + :digits="2" :step="0.01"/> + (default: 1.06) + </div> + <div> + Purdy Points: <b>{{ purdyPoints.toFixed(1) }}</b> + </div> + <div> + V&#775;O&#8322;: <b>{{ vo2.toFixed(1) }}</b> ml/kg/min + (<b>{{ vo2Percentage.toFixed(1) }}%</b> of max) + </div> + <div> + V&#775;O&#8322; Max: <b>{{ vo2Max.toFixed(1) }}</b> ml/kg/min + </div> + </div> + <h2>Equivalent Race Results</h2> - <target-table class="output" :calculate-result="predictTime" :default-targets="defaultTargets" + <target-table class="output" :calculate-result="predictResult" :default-targets="defaultTargets" storage-key="race-calculator-targets" show-pace/> </div> </template> <script> import raceUtils from '@/utils/races'; +import storage from '@/utils/localStorage'; import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; @@ -59,6 +95,21 @@ export default { inputTime: 20 * 60, /** + * The race prediction model + */ + model: storage.get('race-calculator-model', 'AverageModel'), + + /** + * The value of the exponent in Riegel's Model + */ + riegelExponent: storage.get('race-calculator-riegel-exponent', 1.06), + + /** + * Whether to show the advanced options + */ + showAdvancedOptions: storage.get('race-calculator-show-advanced-options', false), + + /** * The names of the distance units */ distanceUnits: unitUtils.DISTANCE_UNITS, @@ -98,14 +149,11 @@ export default { methods: { /** - * Predict race times from a target + * Predict race results from a target * @param {Object} target The target * @returns {Object} The result */ - predictTime(target) { - // Convert input race distance into meters - const d1 = unitUtils.convertDistance(this.inputDistance, this.inputUnit, 'meters'); - + predictResult(target) { // Initialize result const result = { distanceValue: target.distanceValue, @@ -120,13 +168,54 @@ export default { const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters'); // Get prediction - const time = raceUtils.AverageModel.predictTime(d1, this.inputTime, d2); + let time; + switch (this.model) { + default: + case 'AverageModel': + time = raceUtils.AverageModel.predictTime(this.d1, this.inputTime, d2, + this.riegelExponent); + break; + case 'PurdyPointsModel': + time = raceUtils.PurdyPointsModel.predictTime(this.d1, this.inputTime, d2); + break; + case 'VO2MaxModel': + time = raceUtils.VO2MaxModel.predictTime(this.d1, this.inputTime, d2); + break; + case 'RiegelModel': + time = raceUtils.RiegelModel.predictTime(this.d1, this.inputTime, d2, + this.riegelExponent); + break; + case 'CameronModel': + time = raceUtils.CameronModel.predictTime(this.d1, this.inputTime, d2); + break; + } // Update result result.time = time; } else { // Get prediction - let distance = raceUtils.AverageModel.predictDistance(this.inputTime, d1, target.time); + let distance; + switch (this.model) { + default: + case 'AverageModel': + distance = raceUtils.AverageModel.predictDistance(this.inputTime, this.d1, target.time, + this.riegelExponent); + break; + case 'PurdyPointsModel': + distance = raceUtils.PurdyPointsModel.predictDistance(this.inputTime, this.d1, + target.time); + break; + case 'VO2MaxModel': + distance = raceUtils.VO2MaxModel.predictDistance(this.inputTime, this.d1, target.time); + break; + case 'RiegelModel': + distance = raceUtils.RiegelModel.predictDistance(this.inputTime, this.d1, target.time, + this.riegelExponent); + break; + case 'CameronModel': + distance = raceUtils.CameronModel.predictDistance(this.inputTime, this.d1, target.time); + break; + } // Convert output distance into miles distance = unitUtils.convertDistance(distance, 'meters', 'miles'); @@ -140,6 +229,70 @@ export default { return result; }, }, + + computed: { + /** + * The input distance in meters + */ + d1() { + return unitUtils.convertDistance(this.inputDistance, this.inputUnit, 'meters'); + }, + + /** + * The Purdy Points for the input race + */ + purdyPoints() { + const result = raceUtils.PurdyPointsModel.getPurdyPoints(this.d1, this.inputTime); + return result; + }, + + /** + * The VO2 Max calculated from the input race + */ + vo2Max() { + const result = raceUtils.VO2MaxModel.getVO2Max(this.d1, this.inputTime); + return result; + }, + + /** + * The VO2 calculated from the input race + */ + vo2() { + const result = raceUtils.VO2MaxModel.getVO2(this.d1, this.inputTime); + return result; + }, + + /** + * The percentage of VO2 Max calculated from the input race + */ + vo2Percentage() { + const result = raceUtils.VO2MaxModel.getVO2Percentage(this.inputTime) * 100; + return result; + }, + }, + + watch: { + /** + * Save prediction model + */ + model(newValue) { + storage.set('race-calculator-model', newValue); + }, + + /** + * Save Riegel Model exponent + */ + riegelExponent(newValue) { + storage.set('race-calculator-riegel-exponent', newValue); + }, + + /** + * Save advanced options state + */ + showAdvancedOptions(newValue) { + storage.set('race-calculator-show-advanced-options', newValue); + }, + }, }; </script> @@ -168,6 +321,11 @@ h2 { margin-left: 5px; } +/* advanced options */ +.advanced-options>* { + margin-bottom: 5px; +} + /* calculator output */ .output { min-width: 300px; diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -19,7 +19,7 @@ describe('views/RaceCalculator.vue', () => { }); // Predict race times - const result = wrapper.vm.predictTime({ + const result = wrapper.vm.predictResult({ distanceValue: 10, distanceUnit: 'kilometers', result: 'time', @@ -47,7 +47,7 @@ describe('views/RaceCalculator.vue', () => { }); // Predict race distances - const result = wrapper.vm.predictTime({ + const result = wrapper.vm.predictResult({ time: 2460, result: 'distance', });