2024-04-21 14:48:56 +00:00
//This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+
//The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab.
//Other methods are even more approximative.
let wu = require ( "widget_utils" ) ;
let R ;
2023-02-22 21:25:56 +00:00
2024-04-21 15:00:37 +00:00
const ICON _LOCK = atob ( "DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g==" ) ;
2024-04-22 13:29:35 +00:00
const ICON _HEART = atob ( "Dw4BODj4+fv3///////f/z/+P/g/4D+APgA4ACAA" ) ;
const ICON _LOCATION = atob ( "CxABP4/7x/B+D8H8e/5/x/D+D4HwHAEAIA==" ) ;
2024-04-21 15:00:37 +00:00
2024-04-21 14:48:56 +00:00
const x = "x" ; const y = "y" ;
function Rdiv ( axis , divisor ) { // Used when placing things on the screen
return axis == "x" ? ( R . x + ( R . w - 1 ) / divisor ) : ( R . y + ( R . h - 1 ) / divisor ) ;
}
let linePoints = { //Not lists of points, but used to update points in the drawArrows function.
2023-02-22 21:25:56 +00:00
x : [
175 / 40 ,
2 ,
175 / 135 ,
] ,
y : [
175 / 64 ,
175 / 52 ,
175 / 110 ,
175 / 122 ,
2024-04-21 14:48:56 +00:00
]
} ;
2023-02-22 21:25:56 +00:00
2024-04-21 14:48:56 +00:00
function drawArrows ( ) {
g . setColor ( g . theme . fg ) ;
// Upper
g . drawLine ( Rdiv ( x , linePoints . x [ 0 ] ) , Rdiv ( y , linePoints . y [ 0 ] ) , Rdiv ( x , linePoints . x [ 1 ] ) , Rdiv ( y , linePoints . y [ 1 ] ) ) ;
g . drawLine ( Rdiv ( x , linePoints . x [ 1 ] ) , Rdiv ( y , linePoints . y [ 1 ] ) , Rdiv ( x , linePoints . x [ 2 ] ) , Rdiv ( y , linePoints . y [ 0 ] ) ) ;
// Lower
g . drawLine ( Rdiv ( x , linePoints . x [ 0 ] ) , Rdiv ( y , linePoints . y [ 2 ] ) , Rdiv ( x , linePoints . x [ 1 ] ) , Rdiv ( y , linePoints . y [ 3 ] ) ) ;
g . drawLine ( Rdiv ( x , linePoints . x [ 1 ] ) , Rdiv ( y , linePoints . y [ 3 ] ) , Rdiv ( x , linePoints . x [ 2 ] ) , Rdiv ( y , linePoints . y [ 2 ] ) ) ;
}
//To calculate Heart rate zones, we need to know the heart rate reserve (HRR)
// HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr.
//get the hrr (heart rate reserve).
// I put random data here, but this has to come as a menu in the settings section so that users can change it.
let minhr ;
let maxhr ;
let hrr ;
//Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input.
let hr ;
// These letiables display next and previous HR zone.
//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method
//60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack
let minzone2 ;
let maxzone2 ;
let maxzone3 ;
let maxzone4 ;
let maxzone5 ;
function calculatehrr ( minhr , maxhr ) {
hrr = maxhr - minhr ;
minzone2 = hrr * 0.6 + minhr ;
maxzone2 = hrr * 0.7 + minhr ;
maxzone3 = hrr * 0.8 + minhr ;
maxzone4 = hrr * 0.9 + minhr ;
maxzone5 = hrr * 0.99 + minhr ;
}
// HR data: large, readable, in the middle of the screen
function drawHR ( ) {
g . setFontAlign ( - 1 , 0 , 0 ) ;
g . clearRect ( Rdiv ( x , 11 / 4 ) , Rdiv ( y , 2 ) - 25 , Rdiv ( x , 11 / 4 ) + 50 * 2 - 14 , Rdiv ( y , 2 ) + 25 ) ;
g . setColor ( g . theme . fg ) ;
g . setFont ( "Vector" , 50 ) ;
g . drawString ( hr , Rdiv ( x , 11 / 4 ) , Rdiv ( y , 2 ) + 4 ) ;
2024-04-22 14:28:22 +00:00
drawArrows ( ) ;
2024-04-21 14:48:56 +00:00
}
function drawWaitHR ( ) {
g . setColor ( g . theme . fg ) ;
// Waiting for HRM
g . setFontAlign ( 0 , 0 , 0 ) ;
g . setFont ( "Vector" , 50 ) ;
g . drawString ( "--" , Rdiv ( x , 2 ) + 4 , Rdiv ( y , 2 ) + 4 ) ;
// Waiting for current Zone
g . setFont ( "Vector" , 24 ) ;
g . drawString ( "Z-" , Rdiv ( x , 4.3 ) - 3 , Rdiv ( y , 2 ) + 2 ) ;
// waiting for upper and lower limit of current zone
g . setFont ( "Vector" , 20 ) ;
g . drawString ( "--" , Rdiv ( x , 2 ) + 2 , Rdiv ( y , 9 / 2 ) ) ;
g . drawString ( "--" , Rdiv ( x , 2 ) + 2 , Rdiv ( y , 9 / 7 ) ) ;
2024-04-22 14:28:22 +00:00
drawArrows ( ) ;
2024-04-21 14:48:56 +00:00
}
//These functions call arcs to show different HR zones.
//To shorten the code, I'll reference some letiables and reuse them.
let centreX ;
let centreY ;
let minRadius ;
let maxRadius ;
//draw background image (dithered green zones)(I should draw different zones in different dithered colors)
const HRzones = require ( "graphics_utils" ) ;
let minRadiusz ;
let startAngle ;
let endAngle ;
function drawBgArc ( ) {
g . setColor ( g . theme . dark == false ? 0xC618 : "#002200" ) ;
HRzones . fillArc ( g , centreX , centreY , minRadiusz , maxRadius , startAngle , endAngle ) ;
}
const zones = require ( "graphics_utils" ) ;
//####### A function to simplify a bit the code ######
function simplify ( sA , eA , Z , currentZone , lastZone ) {
let startAngle = zones . degreesToRadians ( sA ) ;
let endAngle = zones . degreesToRadians ( eA ) ;
if ( currentZone == lastZone ) zones . fillArc ( g , centreX , centreY , minRadius , maxRadius , startAngle , endAngle ) ;
else zones . fillArc ( g , centreX , centreY , minRadiusz , maxRadius , startAngle , endAngle ) ;
g . setFont ( "Vector" , 24 ) ;
g . clearRect ( Rdiv ( x , 4.3 ) - 12 , Rdiv ( y , 2 ) + 2 - 12 , Rdiv ( x , 4.3 ) + 12 , Rdiv ( y , 2 ) + 2 + 12 ) ;
g . setFontAlign ( 0 , 0 , 0 ) ;
g . drawString ( Z , Rdiv ( x , 4.3 ) , Rdiv ( y , 2 ) + 2 ) ;
}
function zoning ( max , min ) { // draw values of upper and lower limit of current zone
g . setFont ( "Vector" , 20 ) ;
g . setColor ( g . theme . fg ) ;
g . clearRect ( Rdiv ( x , 2 ) - 20 * 2 , Rdiv ( y , 9 / 2 ) - 10 , Rdiv ( x , 2 ) + 20 * 2 , Rdiv ( y , 9 / 2 ) + 10 ) ;
g . clearRect ( Rdiv ( x , 2 ) - 20 * 2 , Rdiv ( y , 9 / 7 ) - 10 , Rdiv ( x , 2 ) + 20 * 2 , Rdiv ( y , 9 / 7 ) + 10 ) ;
g . setFontAlign ( 0 , 0 , 0 ) ;
g . drawString ( max , Rdiv ( x , 2 ) , Rdiv ( y , 9 / 2 ) ) ;
g . drawString ( min , Rdiv ( x , 2 ) , Rdiv ( y , 9 / 7 ) ) ;
}
function clearCurrentZone ( ) { // Clears the extension of the current zone by painting the extension area in background color
g . setColor ( g . theme . bg ) ;
HRzones . fillArc ( g , centreX , centreY , minRadius - 1 , minRadiusz , startAngle , endAngle ) ;
}
2024-04-22 14:28:22 +00:00
function drawZone ( zone ) {
2024-04-21 14:48:56 +00:00
clearCurrentZone ( ) ;
if ( zone >= 0 ) { zoning ( minzone2 , minhr ) ; g . setColor ( "#00ffff" ) ; simplify ( - 88.5 , - 45 , "Z1" , 0 , zone ) ; }
if ( zone >= 1 ) { zoning ( maxzone2 , minzone2 ) ; g . setColor ( "#00ff00" ) ; simplify ( - 43.5 , - 21.5 , "Z2" , 1 , zone ) ; }
if ( zone >= 2 ) { zoning ( maxzone2 , minzone2 ) ; g . setColor ( "#00ff00" ) ; simplify ( - 20 , 1.5 , "Z2" , 2 , zone ) ; }
if ( zone >= 3 ) { zoning ( maxzone2 , minzone2 ) ; g . setColor ( "#00ff00" ) ; simplify ( 3 , 24 , "Z2" , 3 , zone ) ; }
if ( zone >= 4 ) { zoning ( maxzone3 , maxzone2 ) ; g . setColor ( "#ffff00" ) ; simplify ( 25.5 , 46.5 , "Z3" , 4 , zone ) ; }
if ( zone >= 5 ) { zoning ( maxzone3 , maxzone2 ) ; g . setColor ( "#ffff00" ) ; simplify ( 48 , 69 , "Z3" , 5 , zone ) ; }
if ( zone >= 6 ) { zoning ( maxzone3 , maxzone2 ) ; g . setColor ( "#ffff00" ) ; simplify ( 70.5 , 91.5 , "Z3" , 6 , zone ) ; }
if ( zone >= 7 ) { zoning ( maxzone4 , maxzone3 ) ; g . setColor ( "#ff8000" ) ; simplify ( 93 , 114.5 , "Z4" , 7 , zone ) ; }
if ( zone >= 8 ) { zoning ( maxzone4 , maxzone3 ) ; g . setColor ( "#ff8000" ) ; simplify ( 116 , 137.5 , "Z4" , 8 , zone ) ; }
if ( zone >= 9 ) { zoning ( maxzone4 , maxzone3 ) ; g . setColor ( "#ff8000" ) ; simplify ( 139 , 160 , "Z4" , 9 , zone ) ; }
if ( zone >= 10 ) { zoning ( maxzone5 , maxzone4 ) ; g . setColor ( "#ff0000" ) ; simplify ( 161.5 , 182.5 , "Z5" , 10 , zone ) ; }
if ( zone >= 11 ) { zoning ( maxzone5 , maxzone4 ) ; g . setColor ( "#ff0000" ) ; simplify ( 184 , 205 , "Z5" , 11 , zone ) ; }
if ( zone == 12 ) { zoning ( maxzone5 , maxzone4 ) ; g . setColor ( "#ff0000" ) ; simplify ( 206.5 , 227.5 , "Z5" , 12 , zone ) ; }
2023-02-22 21:25:56 +00:00
}
2024-04-22 16:10:16 +00:00
function drawIndicators ( ) {
drawLockIndicator ( ) ;
drawPulseIndicator ( ) ;
drawGpsIndicator ( ) ;
}
2024-04-22 14:28:22 +00:00
function drawZoneAlert ( ) {
2024-04-21 14:48:56 +00:00
const HRzonemax = require ( "graphics_utils" ) ;
2024-04-22 16:10:16 +00:00
drawIndicators ( ) ;
g . clearRect ( R ) ;
2024-04-21 14:48:56 +00:00
let minRadius = 0.40 * R . h ;
let startAngle1 = HRzonemax . degreesToRadians ( - 90 ) ;
let endAngle1 = HRzonemax . degreesToRadians ( 270 ) ;
g . setFont ( "Vector" , 38 ) ; g . setColor ( "#ff0000" ) ;
HRzonemax . fillArc ( g , centreX , centreY , minRadius , maxRadius , startAngle1 , endAngle1 ) ;
g . setFontAlign ( 0 , 0 ) . drawString ( "ALERT" , centreX , centreY ) ;
}
2024-04-22 14:28:22 +00:00
function drawWaitUI ( ) {
g . clearRect ( R ) ;
2024-04-22 16:10:16 +00:00
drawIndicators ( ) ;
2024-04-22 14:28:22 +00:00
drawBgArc ( ) ;
drawWaitHR ( ) ;
drawArrows ( ) ;
}
function drawBase ( ) {
g . clearRect ( R ) ;
2024-04-22 16:10:16 +00:00
drawIndicators ( ) ;
2024-04-22 14:28:22 +00:00
drawBgArc ( ) ;
}
function drawZoneUI ( full ) {
if ( full ) {
drawBase ( ) ;
}
drawHR ( ) ;
drawZones ( ) ;
}
2024-04-21 14:48:56 +00:00
//Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones.
let subZoneLast ;
function drawZones ( ) {
2024-04-22 14:28:22 +00:00
if ( ( hr < maxhr - 2 ) && subZoneLast == 13 ) { drawZoneUI ( true ) ; } // Reset UI when coming down from zone alert.
if ( hr <= hrr * 0.6 + minhr ) { if ( subZoneLast != 0 ) { subZoneLast = 0 ; drawZone ( subZoneLast ) ; } } // Z1
else if ( hr <= hrr * 0.64 + minhr ) { if ( subZoneLast != 1 ) { subZoneLast = 1 ; drawZone ( subZoneLast ) ; } } // Z2a
else if ( hr <= hrr * 0.67 + minhr ) { if ( subZoneLast != 2 ) { subZoneLast = 2 ; drawZone ( subZoneLast ) ; } } // Z2b
else if ( hr <= hrr * 0.70 + minhr ) { if ( subZoneLast != 3 ) { subZoneLast = 3 ; drawZone ( subZoneLast ) ; } } // Z2c
else if ( hr <= hrr * 0.74 + minhr ) { if ( subZoneLast != 4 ) { subZoneLast = 4 ; drawZone ( subZoneLast ) ; } } // Z3a
else if ( hr <= hrr * 0.77 + minhr ) { if ( subZoneLast != 5 ) { subZoneLast = 5 ; drawZone ( subZoneLast ) ; } } // Z3b
else if ( hr <= hrr * 0.80 + minhr ) { if ( subZoneLast != 6 ) { subZoneLast = 6 ; drawZone ( subZoneLast ) ; } } // Z3c
else if ( hr <= hrr * 0.84 + minhr ) { if ( subZoneLast != 7 ) { subZoneLast = 7 ; drawZone ( subZoneLast ) ; } } // Z4a
else if ( hr <= hrr * 0.87 + minhr ) { if ( subZoneLast != 8 ) { subZoneLast = 8 ; drawZone ( subZoneLast ) ; } } // Z4b
else if ( hr <= hrr * 0.90 + minhr ) { if ( subZoneLast != 9 ) { subZoneLast = 9 ; drawZone ( subZoneLast ) ; } } // Z4c
else if ( hr <= hrr * 0.94 + minhr ) { if ( subZoneLast != 10 ) { subZoneLast = 10 ; drawZone ( subZoneLast ) ; } } // Z5a
else if ( hr <= hrr * 0.96 + minhr ) { if ( subZoneLast != 11 ) { subZoneLast = 11 ; drawZone ( subZoneLast ) ; } } // Z5b
else if ( hr <= hrr * 0.98 + minhr ) { if ( subZoneLast != 12 ) { subZoneLast = 12 ; drawZone ( subZoneLast ) ; } } // Z5c
2024-04-22 16:10:16 +00:00
else if ( hr >= maxhr - 2 ) { subZoneLast = 13 ; drawZoneAlert ( ) ; } // Alert
2024-04-21 14:48:56 +00:00
}
let karvonenInterval ;
let hrmstat ;
2024-04-22 13:29:35 +00:00
function drawLockIndicator ( ) {
2024-04-21 15:00:37 +00:00
if ( Bangle . isLocked ( ) )
g . setColor ( g . theme . fg ) . drawImage ( ICON _LOCK , 6 , 8 ) ;
else
g . setColor ( g . theme . bg ) . drawImage ( ICON _LOCK , 6 , 8 ) ;
}
2024-04-22 13:29:35 +00:00
let gpsTimeout ;
2024-04-22 16:10:16 +00:00
let lastGps ;
2024-04-22 13:29:35 +00:00
function drawGpsIndicator ( e ) {
2024-04-22 16:10:16 +00:00
if ( e && e . fix ) {
2024-04-22 13:29:35 +00:00
if ( gpsTimeout )
clearTimeout ( gpsTimeout ) ;
g . setColor ( g . theme . fg ) . drawImage ( ICON _LOCATION , 8 , R . y2 - 23 ) ;
2024-04-22 16:10:16 +00:00
lastGps = 0 ;
2024-04-22 13:29:35 +00:00
gpsTimeout = setTimeout ( ( ) => {
g . setColor ( g . theme . dark ? "#ccc" : "#444" ) . drawImage ( ICON _LOCATION , 8 , R . y2 - 23 ) ;
2024-04-22 16:10:16 +00:00
lastGps = 1 ;
2024-04-22 13:29:35 +00:00
gpsTimeout = setTimeout ( ( ) => {
g . setColor ( g . theme . bg ) . drawImage ( ICON _LOCATION , 8 , R . y2 - 23 ) ;
2024-04-22 16:10:16 +00:00
lastGps = 2 ;
2024-04-22 13:29:35 +00:00
} , 3900 ) ;
} , 1100 ) ;
2024-04-22 16:10:16 +00:00
} else if ( lastGps !== undefined ) {
switch ( lastGps ) {
case 0 : g . setColor ( g . theme . fg ) . drawImage ( ICON _LOCATION , 8 , R . y2 - 23 ) ; break ;
case 1 : g . setColor ( g . theme . dark ? "#ccc" : "#444" ) . drawImage ( ICON _LOCATION , 8 , R . y2 - 23 ) ; break ;
case 2 : g . setColor ( g . theme . bg ) . drawImage ( ICON _LOCATION , 8 , R . y2 - 23 ) ; break ;
}
2024-04-22 13:29:35 +00:00
}
}
let pulseTimeout ;
function drawPulseIndicator ( ) {
if ( hr ) {
if ( pulseTimeout )
clearTimeout ( pulseTimeout ) ;
g . setColor ( g . theme . fg ) . drawImage ( ICON _HEART , R . x2 - 21 , R . y2 - 21 ) ;
pulseTimeout = setTimeout ( ( ) => {
g . setColor ( g . theme . dark ? "#ccc" : "#444" ) . drawImage ( ICON _HEART , R . x2 - 21 , R . y2 - 21 ) ;
pulseTimeout = setTimeout ( ( ) => {
g . setColor ( g . theme . bg ) . drawImage ( ICON _HEART , R . x2 - 21 , R . y2 - 21 ) ;
} , 3900 ) ;
} , 1100 ) ;
}
}
2024-04-21 14:48:56 +00:00
function init ( hrmSettings , exsHrmStats ) {
R = Bangle . appRect ;
hrmstat = exsHrmStats ;
hr = hrmstat . getValue ( ) ;
minhr = hrmSettings . min ;
maxhr = hrmSettings . max ;
calculatehrr ( minhr , maxhr ) ;
centreX = R . x + 0.5 * R . w ;
centreY = R . y + 0.5 * R . h ;
minRadius = 0.38 * R . h ;
maxRadius = 0.50 * R . h ;
minRadiusz = 0.44 * R . h ;
startAngle = HRzones . degreesToRadians ( - 88.5 ) ;
endAngle = HRzones . degreesToRadians ( 268.5 ) ;
}
function start ( hrmSettings , exsHrmStats ) {
wu . hide ( ) ;
init ( hrmSettings , exsHrmStats ) ;
g . reset ( ) . clearRect ( R ) . setFontAlign ( 0 , 0 , 0 ) ;
//draw every second
2024-04-22 13:29:35 +00:00
Bangle . on ( "lock" , drawLockIndicator ) ;
Bangle . on ( "HRM" , drawPulseIndicator ) ;
2024-04-22 14:28:22 +00:00
Bangle . on ( "HRM" , updateUI ) ;
2024-04-22 13:29:35 +00:00
Bangle . on ( "GPS" , drawGpsIndicator ) ;
2024-04-21 14:48:56 +00:00
2024-04-22 14:28:22 +00:00
setTimeout ( drawWaitUI , 0 ) ;
}
let waitTimeout ;
2024-04-21 14:48:56 +00:00
let hrLast ;
//h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below.
function updateUI ( resetHrLast ) { // Update UI, only draw if warranted by change in HR.
if ( resetHrLast ) {
hrLast = 0 ; // Handles correct updating on init depending on if we've got HRM readings yet or not.
subZoneLast = undefined ;
2023-02-22 21:25:56 +00:00
}
2024-04-22 14:28:22 +00:00
if ( waitTimeout ) clearTimeout ( waitTimeout ) ;
waitTimeout = setTimeout ( ( ) => { drawWaitUI ( ) ; waitTimeout = undefined ; } , 2000 ) ;
2024-04-21 14:48:56 +00:00
hr = hrmstat . getValue ( ) ;
//if (h!=0) hr = h;
2024-04-22 14:28:22 +00:00
2024-04-22 16:10:16 +00:00
if ( hrLast != hr || resetHrLast ) {
2024-04-22 14:28:22 +00:00
if ( hr && hrLast != hr ) {
drawZoneUI ( true ) ;
} else if ( ! hr )
drawWaitUI ( ) ;
2023-02-22 21:25:56 +00:00
}
2024-04-22 14:28:22 +00:00
hrLast = hr ;
2024-04-21 14:48:56 +00:00
//g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements.
}
2023-02-22 21:25:56 +00:00
2024-04-21 14:48:56 +00:00
function stop ( ) {
if ( karvonenInterval ) clearInterval ( karvonenInterval ) ;
karvonenInterval = undefined ;
2024-04-22 13:29:35 +00:00
if ( pulseTimeout ) clearTimeout ( pulseTimeout ) ;
pulseTimeout = undefined ;
if ( gpsTimeout ) clearTimeout ( gpsTimeout ) ;
gpsTimeout = undefined ;
2024-04-22 16:10:16 +00:00
if ( waitTimeout ) clearTimeout ( waitTimeout ) ;
waitTimeout = undefined ;
2024-04-22 13:29:35 +00:00
Bangle . removeListener ( "lock" , drawLockIndicator ) ;
Bangle . removeListener ( "GPS" , drawGpsIndicator ) ;
Bangle . removeListener ( "HRM" , drawPulseIndicator ) ;
2024-04-22 14:28:22 +00:00
Bangle . removeListener ( "HRM" , updateUI ) ;
2024-04-21 14:48:56 +00:00
wu . show ( ) ;
}
2023-02-22 21:25:56 +00:00
2024-04-21 14:48:56 +00:00
exports . start = start ;
exports . stop = stop ;