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:
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̇O₂ 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̇O₂: <b>{{ vo2.toFixed(1) }}</b> ml/kg/min
+ (<b>{{ vo2Percentage.toFixed(1) }}%</b> of max)
+ </div>
+ <div>
+ V̇O₂ 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',
});