diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js
index bca8821..48c1620 100644
--- a/app/client/components/PerformanceDashboard.js
+++ b/app/client/components/PerformanceDashboard.js
@@ -7,12 +7,9 @@
import { AppElement, html, css } from './AppElement.js'
import { customElement, state } from 'lit/decorators.js'
-import './DashboardForceCurve.js'
-import './DashboardMetric.js'
-import './DashboardActions.js'
-import './BatteryIcon.js'
import './SettingsDialog'
-import { icon_route, icon_stopwatch, icon_bolt, icon_paddle, icon_heartbeat, icon_fire, icon_clock, icon_settings } from '../lib/icons.js'
+import { icon_settings } from '../lib/icons.js'
+import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js'
@customElement('performance-dashboard')
export class PerformanceDashboard extends AppElement {
@@ -65,39 +62,25 @@ export class PerformanceDashboard extends AppElement {
@state()
_dialog
- dashboardMetricComponents = (formattedMetrics, appState) => ({
- distance: html``,
+ dashboardMetricComponentsFactory = (appState) => {
+ const metrics = appState.metrics
+ const configs = appState.config.guiConfigs
- pace: html``,
+ const dashboardMetricComponents = Object.keys(DASHBOARD_METRICS).reduce((dashboardMetrics, key) => {
+ dashboardMetrics[key] = DASHBOARD_METRICS[key].template(metrics, configs.showIcons)
- power: html``,
+ return dashboardMetrics
+ }, {})
- stkRate: html``,
-
- heartRate: html`
- ${formattedMetrics?.heartrateBatteryLevel?.value
- ? html``
- : ''}
- `,
-
- totalStk: html``,
-
- calories: html``,
-
- timer: html``,
-
- forceCurve: html``,
-
- actions: html``
- })
+ return dashboardMetricComponents
+ }
render () {
const metricConfig = [...new Set(this.appState.config.guiConfigs.dashboardMetrics)].reduce((prev, metricName) => {
- prev.push(this.dashboardMetricComponents(this.metrics, this.appState)[metricName])
+ prev.push(this.dashboardMetricComponentsFactory(this.appState)[metricName])
return prev
}, [])
- this.metrics = this.calculateFormattedMetrics(this.appState.metrics)
return html`
${icon_settings}
@@ -115,33 +98,4 @@ export class PerformanceDashboard extends AppElement {
this._dialog = undefined
}
}
-
- // todo: so far this is just a port of the formatter from the initial proof of concept client
- // we could split this up to make it more readable and testable
- calculateFormattedMetrics (metrics) {
- const fieldFormatter = {
- totalLinearDistanceFormatted: (value) => value >= 10000
- ? { value: (value / 1000).toFixed(2), unit: 'km' }
- : { value: Math.round(value), unit: 'm' },
- totalCalories: (value) => Math.round(value),
- cyclePower: (value) => Math.round(value),
- cycleStrokeRate: (value) => Math.round(value)
- }
-
- const formattedMetrics = {}
- for (const [key, value] of Object.entries(metrics)) {
- const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value
- if (valueFormatted?.value !== undefined && valueFormatted?.unit !== undefined) {
- formattedMetrics[key] = {
- value: valueFormatted?.value,
- unit: valueFormatted?.unit
- }
- } else {
- formattedMetrics[key] = {
- value: valueFormatted
- }
- }
- }
- return formattedMetrics
- }
}
diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js
index d76c1cf..e6b1f7b 100644
--- a/app/client/components/SettingsDialog.js
+++ b/app/client/components/SettingsDialog.js
@@ -9,6 +9,7 @@ import { AppElement, html, css } from './AppElement.js'
import { customElement, property, query, queryAll, state } from 'lit/decorators.js'
import { icon_settings } from '../lib/icons.js'
import './AppDialog.js'
+import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js'
@customElement('settings-dialog')
export class DashboardActions extends AppElement {
@@ -28,9 +29,7 @@ export class DashboardActions extends AppElement {
.settings-dialog>div.metric-selector{
display: grid;
grid-template-columns: repeat(4,max-content);
- grid-template-rows: repeat(3, max-content);
gap: 8px;
-
}
.settings-dialog>div>label{
@@ -112,26 +111,7 @@ export class DashboardActions extends AppElement {
Select metrics to be shown:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ${this.renderAvailableMetricList()}
Slots remaining: ${8 - this._sumSelectedSlots}
@@ -161,6 +141,13 @@ export class DashboardActions extends AppElement {
this._showIconInput.checked = this._showIcons
}
+ renderAvailableMetricList () {
+ return Object.keys(DASHBOARD_METRICS).map(key => html`
+
+
+ `)
+ }
+
renderSelectedMetrics () {
const selectedMetrics = [html`${[0, 1, 2, 3].map(index => html`| ${this._selectedMetrics[index]} | `)}
`]
selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html`| ${this._selectedMetrics[index]} | `)}
`)
diff --git a/app/client/lib/app.js b/app/client/lib/app.js
index 7ce4776..214c4d5 100644
--- a/app/client/lib/app.js
+++ b/app/client/lib/app.js
@@ -8,9 +8,6 @@
import NoSleep from 'nosleep.js'
import { filterObjectByKeys } from './helper.js'
-const rowingMetricsFields = ['totalNumberOfStrokes', 'totalLinearDistanceFormatted', 'totalCalories', 'cyclePower', 'heartrate',
- 'heartrateBatteryLevel', 'cyclePaceFormatted', 'cycleStrokeRate', 'totalMovingTimeFormatted', 'driveHandleForceCurve']
-
export function createApp (app) {
const urlParameters = new URLSearchParams(window.location.search)
const mode = urlParameters.get('mode')
@@ -75,20 +72,7 @@ export function createApp (app) {
break
}
case 'metrics': {
- let activeFields = rowingMetricsFields
- // if we are in reset state only update heart rate and peripheral mode
- if (data.totalNumberOfStrokes < 1) {
- if (data.totalLinearDistanceFormatted > 0) {
- activeFields = ['totalLinearDistanceFormatted', 'heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve']
- } else if (data.totalMovingTimeFormatted !== '00:00') {
- activeFields = ['totalMovingTimeFormatted', 'heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve']
- } else {
- activeFields = ['heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve']
- }
- }
-
- const filteredData = filterObjectByKeys(data, activeFields)
- app.updateState({ ...app.getState(), metrics: filteredData })
+ app.updateState({ ...app.getState(), metrics: data })
break
}
case 'authorizeStrava': {
@@ -124,8 +108,7 @@ export function createApp (app) {
function resetFields () {
const appState = app.getState()
// drop all metrics except heartrate
- appState.metrics = filterObjectByKeys(appState.metrics, ['heartrate', 'heartrateBatteryLevel'])
- app.updateState(appState)
+ app.updateState({ ...appState, metrics: { ...filterObjectByKeys(appState.metrics, ['heartrate', 'heartrateBatteryLevel']) } })
}
function handleAction (action) {
diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js
index 16bae13..1bca643 100644
--- a/app/client/lib/helper.js
+++ b/app/client/lib/helper.js
@@ -1,4 +1,8 @@
'use strict'
+
+import { html } from 'lit'
+import '../components/DashboardMetric.js'
+
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@@ -14,3 +18,55 @@ export function filterObjectByKeys (object, keys) {
return obj
}, {})
}
+
+/**
+ * Pipe for converting seconds to pace format 00:00
+ *
+ * @param seconds The actual time in seconds.
+*/
+export function secondsToPace (seconds) {
+ const hours = Math.floor((seconds % 86400) / 3600)
+ const mins = Math.floor(((seconds % 86400) % 3600) / 60)
+
+ if (seconds === undefined || seconds === null || seconds === Infinity || isNaN(seconds)) return '--'
+ if (hours > 0) {
+ return `${hours}:${mins.toString().padStart(2, '0')}:${(Math.round(seconds) % 60)
+ .toString()
+ .padStart(2, '0')}`
+ } else {
+ return `${mins}:${(Math.round(seconds) % 60).toString().padStart(2, '0')}`
+ }
+}
+
+/**
+ * Pipe for formatting distance in meters with units
+ *
+ * @param value The distance in meters.
+ * @param showInMiles Boolean whether to use imperial metric (default: false).
+*/
+export function formatDistance (value, showInMiles = false) {
+ if (showInMiles === false) {
+ return value >= 10000
+ ? { distance: formatNumber((value / 1000), 2), unit: 'km' }
+ : { distance: formatNumber(value), unit: 'm' }
+ }
+
+ return { distance: formatNumber((value / 1609.344), 2), unit: 'mi' }
+}
+
+/**
+ * Pipe for formatting numbers to specific decimal
+ *
+ * @param value The number.
+ * @param decimalPlaces The number of decimal places to round to (default: 0).
+*/
+export function formatNumber (value, decimalPlaces = 0) {
+ const decimal = decimalPlaces > 0 ? decimalPlaces * 10 : 1
+ if (value === undefined || value === null || value === Infinity || isNaN(value) || value === 0) { return '--' }
+
+ return Math.round(value * decimal) / decimal
+}
+
+export function simpleMetricFactory (value, unit, icon) {
+ return html``
+}
diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js
new file mode 100644
index 0000000..cd6ee12
--- /dev/null
+++ b/app/client/store/dashboardMetrics.js
@@ -0,0 +1,35 @@
+import { html } from 'lit'
+import { simpleMetricFactory, formatDistance, formatNumber, secondsToPace } from '../lib/helper'
+import { icon_bolt, icon_clock, icon_fire, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch } from '../lib/icons'
+import '../components/DashboardForceCurve.js'
+import '../components/DashboardActions.js'
+import '../components/BatteryIcon.js'
+
+export const DASHBOARD_METRICS = {
+ distance: {
+ displayName: 'Distance',
+ size: 1,
+ template: (metrics, showIcon) => {
+ const linearDistance = formatDistance(metrics?.totalLinearDistance)
+
+ return simpleMetricFactory(linearDistance.distance, linearDistance.unit, showIcon ? icon_route : '')
+ }
+ },
+ pace: { displayName: 'Pace/500', size: 1, template: (metrics, showIcon) => simpleMetricFactory(secondsToPace(500 / metrics?.cycleLinearVelocity), '/500m', showIcon ? icon_stopwatch : '') },
+ power: { displayName: 'Power', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.cyclePower), 'watt', showIcon ? icon_bolt : '') },
+ stkRate: { displayName: 'Stroke rate', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.cycleStrokeRate), '/min', showIcon ? icon_paddle : '') },
+ heartRate: {
+ displayName: 'Heart rate',
+ size: 1,
+ template: (metrics, showIcon) => html`
+ ${metrics?.heartrateBatteryLevel
+ ? html``
+ : ''}
+`
+ },
+ totalStk: { displayName: 'Total strokes', size: 1, template: (metrics, showIcon) => simpleMetricFactory(metrics?.totalNumberOfStrokes, 'stk', showIcon ? icon_paddle : '') },
+ calories: { displayName: 'Calories', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.totalCalories), 'kcal', showIcon ? icon_fire : '') },
+ timer: { displayName: 'Timer', size: 1, template: (metrics, showIcon) => simpleMetricFactory(secondsToPace(metrics?.totalMovingTime), '', showIcon ? icon_clock : '') },
+ forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` },
+ actions: { displayName: 'Actions', size: 1, template: () => html`` }
+}