moves formatting of metrics to the displaying components

This commit is contained in:
Lars Berning 2022-01-15 12:29:48 +01:00
parent 424ba431c7
commit f5ff252f95
No known key found for this signature in database
GPG Key ID: 028E73C9E1D8A0B3
6 changed files with 78 additions and 58 deletions

View File

@ -21,7 +21,7 @@ export class DashboardActions extends AppElement {
<button @click=${this.reset}>${icon_undo}</button> <button @click=${this.reset}>${icon_undo}</button>
${this.renderOptionalButtons()} ${this.renderOptionalButtons()}
<button @click=${this.switchPeripheralMode}>${icon_bluetooth}</button> <button @click=${this.switchPeripheralMode}>${icon_bluetooth}</button>
<div class="metric-unit">${this.appState?.metrics?.peripheralMode?.value}</div> <div class="metric-unit">${this.peripheralMode()}</div>
` `
} }
@ -29,7 +29,7 @@ export class DashboardActions extends AppElement {
const buttons = [] const buttons = []
// changing to fullscreen mode only makes sence when the app is openend in a regular // changing to fullscreen mode only makes sence when the app is openend in a regular
// webbrowser (kiosk and standalone mode are always in fullscreen view) // webbrowser (kiosk and standalone mode are always in fullscreen view)
if (this.appState.appMode === '') { if (this.appState.appMode === 'BROWSER') {
buttons.push(html` buttons.push(html`
<button @click=${this.toggleFullscreen}> <button @click=${this.toggleFullscreen}>
<div id="fullscreen-icon">${icon_expand}</div> <div id="fullscreen-icon">${icon_expand}</div>
@ -48,6 +48,17 @@ export class DashboardActions extends AppElement {
return buttons return buttons
} }
peripheralMode () {
const value = this.appState?.peripheralMode
if (value === 'PM5') {
return 'C2 PM5'
} else if (value === 'FTMSBIKE') {
return 'FTMS Bike'
} else {
return 'FTMS Rower'
}
}
toggleFullscreen () { toggleFullscreen () {
const fullscreenElement = document.getElementsByTagName('web-app')[0] const fullscreenElement = document.getElementsByTagName('web-app')[0]
if (!document.fullscreenElement) { if (!document.fullscreenElement) {

View File

@ -31,7 +31,7 @@ export class DashboardMetric extends AppElement {
return html` return html`
<div class="label">${this.icon}</div> <div class="label">${this.icon}</div>
<div class="content"> <div class="content">
<span class="metric-value">${this.value}</span> <span class="metric-value">${this.value !== undefined ? this.value : '--'}</span>
<span class="metric-unit">${this.unit}</span> <span class="metric-unit">${this.unit}</span>
</div> </div>
${this.batteryLevel && ${this.batteryLevel &&

View File

@ -26,7 +26,7 @@ export class PerformanceDashboard extends AppElement {
appState = APP_STATE appState = APP_STATE
render () { render () {
const metrics = this.appState.metrics const metrics = this.calculateFormattedMetrics(this.appState.metrics)
return html` return html`
<dashboard-metric .icon=${icon_route} .unit=${metrics?.distanceTotal?.unit} .value=${metrics?.distanceTotal?.value}></dashboard-metric> <dashboard-metric .icon=${icon_route} .unit=${metrics?.distanceTotal?.unit} .value=${metrics?.distanceTotal?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${metrics?.splitFormatted?.value}></dashboard-metric> <dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${metrics?.splitFormatted?.value}></dashboard-metric>
@ -42,4 +42,31 @@ export class PerformanceDashboard extends AppElement {
<dashboard-actions .appState=${this.appState}></dashboard-actions> <dashboard-actions .appState=${this.appState}></dashboard-actions>
` `
} }
calculateFormattedMetrics (metrics) {
const fieldFormatter = {
distanceTotal: (value) => value >= 10000
? { value: (value / 1000).toFixed(1), unit: 'km' }
: { value: Math.round(value), unit: 'm' },
caloriesTotal: (value) => Math.round(value),
power: (value) => Math.round(value),
strokesPerMinute: (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
}
} }

View File

@ -47,12 +47,13 @@ export class App extends LitElement {
// i.e. do something like this.appState = { ..this.appState, ...newState } // i.e. do something like this.appState = { ..this.appState, ...newState }
updateState = (newState) => { updateState = (newState) => {
this.appState = { ...newState } this.appState = { ...newState }
// console.table(this.appState.metrics)
} }
// return a copy of the state to other components to minimize risk of side effects // return a deep copy of the state to other components to minimize risk of side effects
getState = () => { getState = () => {
return { ...this.appState } // todo: could use structuredClone once the browser support is wider
// https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
return JSON.parse(JSON.stringify(this.appState))
} }
// once we have multiple views, then we would rather reference some kind of router here // once we have multiple views, then we would rather reference some kind of router here

View File

@ -7,32 +7,13 @@
import NoSleep from 'nosleep.js' import NoSleep from 'nosleep.js'
export function createApp (app) { const rowingMetricsFields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'heartrate',
const fields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'heartrate', 'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted']
'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted', 'peripheralMode']
// todo: formatting should happen in the related components (i.e.) PerformanceDashboard
const fieldFormatter = {
peripheralMode: (value) => {
if (value === 'PM5') {
return 'C2 PM5'
} else if (value === 'FTMSBIKE') {
return 'FTMS Bike'
} else {
return 'FTMS Rower'
}
},
distanceTotal: (value) => value >= 10000
? { value: (value / 1000).toFixed(1), unit: 'km' }
: { value: Math.round(value), unit: 'm' },
caloriesTotal: (value) => Math.round(value),
power: (value) => Math.round(value),
strokesPerMinute: (value) => Math.round(value)
}
const mode = window.location.hash
const appMode = mode === '#:standalone:' ? 'STANDALONE' : mode === '#:kiosk:' ? 'KIOSK' : ''
app.updateState({ ...app.getState(), appMode })
const metrics = {} export function createApp (app) {
const mode = window.location.hash
const appMode = mode === '#:standalone:' ? 'STANDALONE' : mode === '#:kiosk:' ? 'KIOSK' : 'BROWSER'
app.updateState({ ...app.getState(), appMode })
let socket let socket
@ -61,34 +42,29 @@ export function createApp (app) {
}, 1000) }, 1000)
}) })
// todo: we have to use different types of messages to make processing easier
socket.addEventListener('message', (event) => { socket.addEventListener('message', (event) => {
try { try {
const data = JSON.parse(event.data) const data = JSON.parse(event.data)
let activeFields = fields let activeFields = rowingMetricsFields
// if we are in reset state only update heart rate and peripheral mode // if we are in reset state only update heart rate
if (data.strokesTotal === 0) { if (data.strokesTotal === 0) {
activeFields = ['heartrate', 'peripheralMode'] activeFields = ['heartrate']
} }
// todo: formatting should happen in the related components (i.e.) PerformanceDashboard const filteredData = Object.keys(data)
for (const [key, value] of Object.entries(data)) { .filter(key => activeFields.includes(key))
if (activeFields.includes(key)) { .reduce((obj, key) => {
const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value obj[key] = data[key]
if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) { return obj
metrics[key] = { }, {})
value: valueFormatted.value,
unit: valueFormatted.unit
}
} else {
metrics[key] = {
value: valueFormatted
}
}
}
}
app.updateState({ ...app.getState(), metrics }) let updatedState = { ...app.getState(), metrics: filteredData }
if (data.peripheralMode) {
updatedState = { ...app.getState(), peripheralMode: data.peripheralMode }
}
app.updateState(updatedState)
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
@ -111,10 +87,15 @@ export function createApp (app) {
} }
function resetFields () { function resetFields () {
for (const key of fields.filter((elem) => elem !== 'peripheralMode' && elem !== 'heartrate')) { const appState = app.getState()
metrics[key] = { value: '--' } // drop all metrics except heartrate
} appState.metrics = Object.keys(appState.metrics)
app.updateState({ ...app.getState(), metrics }) .filter(key => key === 'heartrate' || key === 'heartrateBatteryLevel')
.reduce((obj, key) => {
obj[key] = appState.metrics[key]
return obj
}, {})
app.updateState(appState)
} }
function handleAction (action) { function handleAction (action) {

View File

@ -8,8 +8,8 @@
export const APP_STATE = { export const APP_STATE = {
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default) // currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
appMode: '', appMode: '',
// todo: this is currently embedded into the metrics object, but should probably be extracted // currently can be FTMS, FTMSBIKE or PM5
peripheralMode: 'FTMSROWER', peripheralMode: 'FTMS',
// contains all the rowing metrics that are delivered from the backend // contains all the rowing metrics that are delivered from the backend
metrics: {} metrics: {}
} }