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``)}`] selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html``)}`) 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`` } +}
${this._selectedMetrics[index]}
${this._selectedMetrics[index]}