openrowingmonitor/app/client/components/PerformanceDashboard.js

155 lines
5.6 KiB
JavaScript

'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Component that renders the dashboard
*/
import { AppElement, html, css } from './AppElement.js'
import { APP_STATE } from '../store/appState.js'
import { customElement, property, 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'
@customElement('performance-dashboard')
export class PerformanceDashboard extends AppElement {
static styles = css`
:host {
display: grid;
height: calc(100vh - 2vw);
padding: 1vw;
grid-gap: 1vw;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
}
@media (orientation: portrait) {
:host {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(4, minmax(0, 1fr));
}
}
dashboard-metric, dashboard-actions, dashboard-force-curve {
background: var(--theme-widget-color);
text-align: center;
position: relative;
padding: 0.5em 0.2em 0 0.2em;
border-radius: var(--theme-border-radius);
}
dashboard-actions {
padding: 0.5em 0 0 0;
}
.settings {
padding: 0.1em 0;
position: absolute;
bottom: 0;
right: 0;
z-index: 20;
}
.settings .icon {
cursor: pointer;
height: 1em;
}
.settings:hover .icon {
filter: brightness(150%);
}
`
dashboardMetricComponents = (formattedMetrics, appState) => ({
distance: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_route : ''} .unit=${formattedMetrics?.totalLinearDistanceFormatted?.unit || 'm'} .value=${formattedMetrics?.totalLinearDistanceFormatted?.value}></dashboard-metric>`,
pace: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_stopwatch : ''} unit="/500m" .value=${formattedMetrics?.cyclePaceFormatted?.value}></dashboard-metric>`,
power: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_bolt : ''} unit="watt" .value=${formattedMetrics?.cyclePower?.value}></dashboard-metric>`,
stkRate: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_paddle : ''} unit="/min" .value=${formattedMetrics?.cycleStrokeRate?.value}></dashboard-metric>`,
heartRate: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_heartbeat : ''} unit="bpm" .value=${formattedMetrics?.heartrate?.value}>
${formattedMetrics?.heartrateBatteryLevel?.value
? html`<battery-icon .batteryLevel=${formattedMetrics?.heartrateBatteryLevel?.value}></battery-icon>`
: ''}
</dashboard-metric>`,
totalStk: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_paddle : ''} unit="total" .value=${formattedMetrics?.totalNumberOfStrokes?.value}></dashboard-metric>`,
calories: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_fire : ''} unit="kcal" .value=${formattedMetrics?.totalCalories?.value}></dashboard-metric>`,
timer: html`<dashboard-metric .icon=${this.appState.config.guiConfigs.showIcons ? icon_clock : ''} .value=${formattedMetrics?.totalMovingTimeFormatted?.value}></dashboard-metric>`,
forceCurve: html`<dashboard-force-curve .value=${appState?.metrics.driveHandleForceCurve} style="grid-column: span 2"></dashboard-force-curve>`,
actions: html`<dashboard-actions .appState=${appState}></dashboard-actions>`
})
@state({ type: Object })
dialog
@property({ type: Object })
metrics
@property({ type: Object })
appState = APP_STATE
render () {
const metricConfig = [...new Set(this.appState.config.guiConfigs.dashboardMetrics)].reduce((prev, metricName) => {
prev.push(this.dashboardMetricComponents(this.metrics, this.appState)[metricName])
return prev
}, [])
this.metrics = this.calculateFormattedMetrics(this.appState.metrics)
return html`
<div class="settings" @click=${this.openSettings}>
${icon_settings}
${this.dialog ? this.dialog : ''}
</div>
${metricConfig}
`
}
openSettings () {
this.dialog = html`<settings-dialog .config=${this.appState.config.guiConfigs} @close=${dialogClosed}></settings-dialog>`
function dialogClosed (event) {
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
}
}