diff --git a/always-on-source/AlwaysOnDisplay.mc b/always-on-source/AlwaysOnDisplay.mc index eb1f5093..d5be9952 100644 --- a/always-on-source/AlwaysOnDisplay.mc +++ b/always-on-source/AlwaysOnDisplay.mc @@ -1,201 +1,212 @@ -using Toybox.WatchUi as Ui; -using Toybox.System as Sys; -using Toybox.Application as App; -using Toybox.Time; -using Toybox.Time.Gregorian; - -// Draw time, line, date, battery. -// Combine stripped down versions of ThickThinTime and DateLine. -// Change vertical offset every minute to comply with burn-in protection requirements. -class AlwaysOnDisplay extends Ui.Drawable { - - private var mBurnInYOffsets; - private var mHoursFont, mMinutesFont, mSecondsFont, mDateFont, mBatteryFont; - - // Wide rectangle: time should be moved up slightly to centre within available space. - private var mAdjustY = 0; - - private var mTimeY; - private var mLineY; - private var mLineWidth; - private var mLineStroke; - private var mDataY; - private var mDataLeft; - - private var AM_PM_X_OFFSET = 2; - - private var mDayOfWeek; - private var mDayOfWeekString; - - private var mMonth; - private var mMonthString; - - function initialize(params) { - Drawable.initialize(params); - - mBurnInYOffsets = params[:burnInYOffsets]; - - if (params[:adjustY] != null) { - mAdjustY = params[:adjustY]; - } - - if (params[:amPmOffset] != null) { - AM_PM_X_OFFSET = params[:amPmOffset]; - } - - mTimeY = params[:timeY]; - mLineY = params[:lineY]; - mLineWidth = params[:lineWidth]; - //mLineStroke = params[:lineStroke]; - mDataY = params[:dataY]; - mDataLeft = params[:dataLeft]; - - mHoursFont = Ui.loadResource(Rez.Fonts.AlwaysOnHoursFont); - mMinutesFont = Ui.loadResource(Rez.Fonts.AlwaysOnMinutesFont); - mSecondsFont = Ui.loadResource(Rez.Fonts.AlwaysOnSecondsFont); - mBatteryFont = Ui.loadResource(Rez.Fonts.AlwaysOnBatteryFont); - - var rezFonts = Rez.Fonts; - var resourceMap = { - "ZHS" => rezFonts.AlwaysOnDateFontOverrideZHS, - "ZHT" => rezFonts.AlwaysOnDateFontOverrideZHT, - "RUS" => rezFonts.AlwaysOnDateFontOverrideRUS - }; - - // Unfortunate: because fonts can't be overridden based on locale, we have to read in current locale as manually-specified - // string, then override font in code. - var dateFontOverride = Ui.loadResource(Rez.Strings.DATE_FONT_OVERRIDE); - var dateFont = (resourceMap.hasKey(dateFontOverride)) ? resourceMap[dateFontOverride] : rezFonts.AlwaysOnDateFont; - mDateFont = Ui.loadResource(dateFont); - } - - function draw(dc) { - - // TIME. - var clockTime = Sys.getClockTime(); - var formattedTime = App.getApp().getFormattedTime(clockTime.hour, clockTime.min); - formattedTime[:amPm] = formattedTime[:amPm].toUpper(); - - // Change vertical offset every minute. - var burnInYOffset = mBurnInYOffsets[clockTime.min % mBurnInYOffsets.size()] + (clockTime.min - 30); - - var hours = formattedTime[:hour]; - var minutes = formattedTime[:min]; - var amPmText = formattedTime[:amPm]; - - var halfDCWidth = dc.getWidth() / 2; - - // Centre combined hours and minutes text (not the same as right-aligning hours and left-aligning minutes). - // Font has tabular figures (monospaced numbers) even across different weights, so does not matter which of hours or - // minutes font is used to calculate total width. - var totalWidth = dc.getTextWidthInPixels(hours + minutes, mHoursFont); - var x = halfDCWidth - (totalWidth / 2); - var y = mTimeY + mAdjustY + burnInYOffset; - - dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); - - // Hours. - dc.drawText( - x, - y, - mHoursFont, - hours, - Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER - ); - x += dc.getTextWidthInPixels(hours, mHoursFont); - - // Minutes. - dc.drawText( - x, - y, - mMinutesFont, - minutes, - Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER - ); - - // If required, draw AM/PM after minutes, vertically centred. - if (amPmText.length() > 0) { - x += dc.getTextWidthInPixels(minutes, mMinutesFont); - dc.drawText( - x + AM_PM_X_OFFSET, // Breathing space between minutes and AM/PM. - y, - mSecondsFont, - amPmText, - Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER - ); - } - - // LINE. - y = mLineY + burnInYOffset; - dc.setPenWidth(/* mLineStroke */ 2); - dc.drawLine(halfDCWidth - (mLineWidth / 2), y, halfDCWidth + (mLineWidth / 2), y); - - // DATA. - var rezStrings = Rez.Strings; - var resourceArray; - - // Supply DOW/month strings ourselves, rather than relying on Time.FORMAT_MEDIUM, as latter is inconsistent e.g. returns - // "Thurs" instead of "Thu". - // Load strings just-in-time, to save memory. They rarely change, so worthwhile trade-off. - var now = Gregorian.info(Time.now(), Time.FORMAT_SHORT); - - var dayOfWeek = now.day_of_week; - if (dayOfWeek != mDayOfWeek) { - mDayOfWeek = dayOfWeek; - - resourceArray = [ - rezStrings.Sun, - rezStrings.Mon, - rezStrings.Tue, - rezStrings.Wed, - rezStrings.Thu, - rezStrings.Fri, - rezStrings.Sat - ]; - mDayOfWeekString = Ui.loadResource(resourceArray[mDayOfWeek - 1]).toUpper(); - } - - var month = now.month; - if (month != mMonth) { - mMonth = month; - - resourceArray = [ - rezStrings.Jan, - rezStrings.Feb, - rezStrings.Mar, - rezStrings.Apr, - rezStrings.May, - rezStrings.Jun, - rezStrings.Jul, - rezStrings.Aug, - rezStrings.Sep, - rezStrings.Oct, - rezStrings.Nov, - rezStrings.Dec - ]; - mMonthString = Ui.loadResource(resourceArray[mMonth - 1]).toUpper(); - } - - var day = now.day.format(INTEGER_FORMAT); - - // Date. - y = mDataY + burnInYOffset; - dc.drawText( - mDataLeft, - y, - mDateFont, - Lang.format("$1$ $2$ $3$", [mDayOfWeekString, day, mMonthString]), - Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER - ); - - // Battery. - var battery = Math.floor(Sys.getSystemStats().battery); - dc.drawText( - dc.getWidth() - mDataLeft, - y, - mBatteryFont, - battery.format(INTEGER_FORMAT) + "%", - Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER - ); - } +using Toybox.WatchUi as Ui; +using Toybox.System as Sys; +using Toybox.Application as App; +using Toybox.Time; +using Toybox.Time.Gregorian; + +// Draw time, line, date, battery. +// Combine stripped down versions of ThickThinTime and DateLine. +// Change vertical offset every minute to comply with burn-in protection requirements. +class AlwaysOnDisplay extends Ui.Drawable { + + private var mBurnInYOffsets; + private var mHoursFont, mMinutesFont, mSecondsFont, mDateFont, mBatteryFont; + + // Wide rectangle: time should be moved up slightly to centre within available space. + private var mAdjustY = 0; + + private var mTimeY; + private var mLineY; + private var mLineWidth; + private var mLineStroke; + private var mDataY; + private var mDataLeft; + + private var AM_PM_X_OFFSET = 2; + + private var mDayOfWeek; + private var mDayOfWeekString; + + private var mMonth; + private var mMonthString; + + function initialize(params) { + Drawable.initialize(params); + + mBurnInYOffsets = params[:burnInYOffsets]; + + if (params[:adjustY] != null) { + mAdjustY = params[:adjustY]; + } + + if (params[:amPmOffset] != null) { + AM_PM_X_OFFSET = params[:amPmOffset]; + } + + mTimeY = params[:timeY]; + mLineY = params[:lineY]; + mLineWidth = params[:lineWidth]; + //mLineStroke = params[:lineStroke]; + mDataY = params[:dataY]; + mDataLeft = params[:dataLeft]; + + mHoursFont = Ui.loadResource(Rez.Fonts.AlwaysOnHoursFont); + mMinutesFont = Ui.loadResource(Rez.Fonts.AlwaysOnMinutesFont); + mSecondsFont = Ui.loadResource(Rez.Fonts.AlwaysOnSecondsFont); + mBatteryFont = Ui.loadResource(Rez.Fonts.AlwaysOnBatteryFont); + + var rezFonts = Rez.Fonts; + var resourceMap = { + "ZHS" => rezFonts.AlwaysOnDateFontOverrideZHS, + "ZHT" => rezFonts.AlwaysOnDateFontOverrideZHT, + "RUS" => rezFonts.AlwaysOnDateFontOverrideRUS + }; + + // Unfortunate: because fonts can't be overridden based on locale, we have to read in current locale as manually-specified + // string, then override font in code. + var dateFontOverride = Ui.loadResource(Rez.Strings.DATE_FONT_OVERRIDE); + var dateFont = (resourceMap.hasKey(dateFontOverride)) ? resourceMap[dateFontOverride] : rezFonts.AlwaysOnDateFont; + mDateFont = Ui.loadResource(dateFont); + } + + function draw(dc) { + + // TIME. + var clockTime = Sys.getClockTime(); + var formattedTime = App.getApp().getFormattedTime(clockTime.hour, clockTime.min); + formattedTime[:amPm] = formattedTime[:amPm].toUpper(); + + // Change vertical offset every minute. + var burnInYOffset = mBurnInYOffsets[clockTime.min % mBurnInYOffsets.size()] + (clockTime.min - 30); + + var hours = formattedTime[:hour]; + var minutes = formattedTime[:min]; + var amPmText = formattedTime[:amPm]; + var colon = ":"; // SG Addition + + var halfDCWidth = dc.getWidth() / 2; + + // Centre combined hours and minutes text (not the same as right-aligning hours and left-aligning minutes). + // Font has tabular figures (monospaced numbers) even across different weights, so does not matter which of hours or + // minutes font is used to calculate total width. + var totalWidth = dc.getTextWidthInPixels(hours + colon + minutes, mHoursFont); // SG Added colon + var x = halfDCWidth - (totalWidth / 2); + var y = mTimeY + mAdjustY + burnInYOffset; + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + + // Hours. + dc.drawText( + x, + y, + mHoursFont, + hours, + Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER + ); + x += dc.getTextWidthInPixels(hours, mHoursFont); + + // SG Addition - Colon. + dc.drawText( + x, + y, + mHoursFont, + colon, + Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER + ); + x += dc.getTextWidthInPixels(colon, mHoursFont); + + // Minutes. + dc.drawText( + x, + y, + mMinutesFont, + minutes, + Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER + ); + + // If required, draw AM/PM after minutes, vertically centred. + if (amPmText.length() > 0) { + x += dc.getTextWidthInPixels(minutes, mMinutesFont); + dc.drawText( + x + AM_PM_X_OFFSET, // Breathing space between minutes and AM/PM. + y, + mSecondsFont, + amPmText, + Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER + ); + } + + // LINE. + y = mLineY + burnInYOffset; + dc.setPenWidth(/* mLineStroke */ 1); // SG From 2 to 1 + dc.drawLine(halfDCWidth - (mLineWidth / 2), y, halfDCWidth + (mLineWidth / 2), y); + + // DATA. + var rezStrings = Rez.Strings; + var resourceArray; + + // Supply DOW/month strings ourselves, rather than relying on Time.FORMAT_MEDIUM, as latter is inconsistent e.g. returns + // "Thurs" instead of "Thu". + // Load strings just-in-time, to save memory. They rarely change, so worthwhile trade-off. + var now = Gregorian.info(Time.now(), Time.FORMAT_SHORT); + + var dayOfWeek = now.day_of_week; + if (dayOfWeek != mDayOfWeek) { + mDayOfWeek = dayOfWeek; + + resourceArray = [ + rezStrings.Sun, + rezStrings.Mon, + rezStrings.Tue, + rezStrings.Wed, + rezStrings.Thu, + rezStrings.Fri, + rezStrings.Sat + ]; + mDayOfWeekString = Ui.loadResource(resourceArray[mDayOfWeek - 1]).toUpper(); + } + + var month = now.month; + if (month != mMonth) { + mMonth = month; + + resourceArray = [ + rezStrings.Jan, + rezStrings.Feb, + rezStrings.Mar, + rezStrings.Apr, + rezStrings.May, + rezStrings.Jun, + rezStrings.Jul, + rezStrings.Aug, + rezStrings.Sep, + rezStrings.Oct, + rezStrings.Nov, + rezStrings.Dec + ]; + mMonthString = Ui.loadResource(resourceArray[mMonth - 1]).toUpper(); + } + + var day = now.day.format(INTEGER_FORMAT); + + // Date. + y = mDataY + burnInYOffset; + dc.drawText( + mDataLeft, + y, + mDateFont, + Lang.format("$1$ $2$ $3$", [mDayOfWeekString, day, mMonthString]), + Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER + ); + + // Battery. + var battery = Math.floor(Sys.getSystemStats().battery); + dc.drawText( + dc.getWidth() - mDataLeft, + y, + mBatteryFont, + battery.format(INTEGER_FORMAT) + "%", + Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER + ); + } } \ No newline at end of file diff --git a/resources-fre/strings/strings.xml b/resources-fre/strings/strings.xml index 87dab35c..168ae70a 100644 --- a/resources-fre/strings/strings.xml +++ b/resources-fre/strings/strings.xml @@ -1,81 +1,81 @@ Crystal - App Version + Version de l'appli - Theme - Hours Colour - Minutes Colour - Left Meter - Right Meter - Calories Goal (1-10,000kCal) - Meter Style - Meter Digits Style - Add Local Time in City (Beta) - Move Bar Style - Hide Seconds - Hide Hours Leading Zero + Thème + Couleur des heures + Couleur des minutes + Compteur de gauche + Compteur de droite + Objectif de calories (1-10,000kCal) + Apparence des compteurs + Apparence des champs numériques + Ajout de l'heure d'une ville (Beta) + Apparence de la barre d'activité + Masquer les secondes + Masquer le zéro avant l'heure - Number Of Data Fields - Data Field 1 - Data Field 2 - Data Field 3 + Nombre de champs de données + Champs de données 1 + Champs de données 2 + Champs de données 3 - Number Of Indicators - Indicator 1 - Indicator 2 - Indicator 3 + Nombre d'indicateurs + Indicateur 1 + Indicateur 2 + Indicateur 3 - (From Theme) - Mono Highlight - Mono + (À partir du thème) + Monochrome brillant + Monochrome - All Segments (Merged) - Filled Segments (Merged) - All Segments - Filled Segments - Hidden - Current/Target - Current + Tous les segments (fusionnés) + Segments remplis seulement (fusionnés) + Tous les segments + Segments remplis seulement + Masqué + Actuel/Objectif + Actuel - Blue (Dark) - Pink (Dark) - Red (Dark) - Green (Dark) - Cornflower Blue (Dark) - Lemon Cream (Dark) - Vivid Yellow (Dark) - Dayglo Orange (Dark) - Mono (Dark) - Mono (Light) - Blue (Light) - Red (Light) - Green (Light) - Dayglo Orange (Light) - Corn Yellow (Dark) + Bleu (sur fond noir) + Rose (sur fond noir) + Rouge (sur fond noir) + Vert(sur fond noir) + Bleu pale (sur fond noir) + Crème (sur fond noir) + Jaune vif (sur fond noir) + Orange (sur fond noir) + Monochrome (sur fond noir) + Monochrome (sur fond blanc) + Bleu (sur fond blanc) + Rouge (sur fond blanc) + Vert (sur fond blanc) + Orange (sur fond blanc) + Jaune foncé (sur fond noir) - Steps - Floors Climbed - Active Minutes (Weekly) - Calories (Manual Goal) - Off + Pas + Étages montées + Minutes actives (hebdo) + Calories (objectif manuel) + Masqué - Heart Rate - Heart Rate (Live 5s) - Battery - Battery (Hide Percentage) + Rythme cardiaque + Rythme cardiaque (actif 5s) + Pile + Pile (Pourcentage masqué) Notifications Calories Distance - Alarms + Alarmes Altitude - Thermometer + Thermomètre Bluetooth Bluetooth/Notifications - Sunrise/Sunset - Weather - Humidity - Pressure + Lever/Coucher du soleil + Météo + Humidité + Pression @@ -89,16 +89,16 @@ Sam Jan - Fev + Fév Mar Avr Mai Juin Juil - Aout + Août Sep Oct Nov - Dec + Déc diff --git a/resources-round-280x280/fonts/crystal-icons-large.fnt b/resources-round-280x280/fonts/crystal-icons-large.fnt index 3fc6ac62..b82a0d52 100644 --- a/resources-round-280x280/fonts/crystal-icons-large.fnt +++ b/resources-round-280x280/fonts/crystal-icons-large.fnt @@ -1,7 +1,7 @@ info face="Crystal Icons" size=-28 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0 common lineHeight=28 base=28 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0 page id=0 file="crystal-icons-large.png" -chars count=17 +chars count=18 char id=48 x=0 y=0 width=25 height=28 xoffset=0 yoffset=0 xadvance=25 page=0 chnl=15 char id=49 x=25 y=0 width=28 height=28 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15 char id=50 x=53 y=0 width=28 height=28 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15 @@ -18,4 +18,5 @@ char id=61 x=42 y=28 width=7 height=7 xoffset=0 yoffset=10 char id=62 x=49 y=28 width=32 height=28 xoffset=0 yoffset=0 xadvance=32 page=0 chnl=15 char id=63 x=81 y=28 width=32 height=28 xoffset=0 yoffset=0 xadvance=32 page=0 chnl=15 char id=64 x=113 y=28 width=24 height=28 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15 -char id=65 x=137 y=28 width=19 height=28 xoffset=0 yoffset=0 xadvance=19 page=0 chnl=15 \ No newline at end of file +char id=65 x=137 y=28 width=19 height=28 xoffset=0 yoffset=0 xadvance=19 page=0 chnl=15 +char id=66 x=158 y=32 width=28 height=25 xoffset=0 yoffset=0 xadvance=32 page=0 chnl=15 \ No newline at end of file diff --git a/resources-round-280x280/fonts/crystal-icons-large.png b/resources-round-280x280/fonts/crystal-icons-large.png index c8626ea6..db99c151 100644 Binary files a/resources-round-280x280/fonts/crystal-icons-large.png and b/resources-round-280x280/fonts/crystal-icons-large.png differ diff --git a/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc b/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc index 07c3eb7a..ac13b0e6 100644 --- a/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc +++ b/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc @@ -1,55 +1,55 @@ -# AngelCode Bitmap Font Generator configuration file -fileVersion=1 - -# font settings -fontName=Titillium Web Light -fontFile= -charSet=0 -fontSize=-80 -aa=1 -scaleH=105 -useSmoothing=1 -isBold=0 -isItalic=0 -useUnicode=1 -disableBoxChars=1 -outputInvalidCharGlyph=0 -dontIncludeKerningPairs=0 -useHinting=1 -renderFromOutline=0 -useClearType=0 - -# character alignment -paddingDown=0 -paddingUp=0 -paddingRight=0 -paddingLeft=0 -spacingHoriz=1 -spacingVert=1 -useFixedHeight=0 -forceZero=0 - -# output file -outWidth=256 -outHeight=256 -outBitDepth=8 -fontDescFormat=0 -fourChnlPacked=0 -textureFormat=png -textureCompression=0 -alphaChnl=1 -redChnl=0 -greenChnl=0 -blueChnl=0 -invA=0 -invR=0 -invG=0 -invB=0 - -# outline -outlineThickness=0 - -# selected chars -chars=48-57 - -# imported icon images +# AngelCode Bitmap Font Generator configuration file +fileVersion=1 + +# font settings +fontName=Titillium Web Light +fontFile= +charSet=0 +fontSize=-80 +aa=1 +scaleH=105 +useSmoothing=1 +isBold=0 +isItalic=0 +useUnicode=1 +disableBoxChars=1 +outputInvalidCharGlyph=0 +dontIncludeKerningPairs=0 +useHinting=1 +renderFromOutline=0 +useClearType=0 + +# character alignment +paddingDown=0 +paddingUp=0 +paddingRight=0 +paddingLeft=0 +spacingHoriz=1 +spacingVert=1 +useFixedHeight=0 +forceZero=0 + +# output file +outWidth=256 +outHeight=256 +outBitDepth=8 +fontDescFormat=0 +fourChnlPacked=0 +textureFormat=png +textureCompression=0 +alphaChnl=1 +redChnl=0 +greenChnl=0 +blueChnl=0 +invA=0 +invR=0 +invG=0 +invB=0 + +# outline +outlineThickness=0 + +# selected chars +chars=48-58 + +# imported icon images diff --git a/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt b/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt index 049b1dfe..ec6b4a6a 100644 --- a/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt +++ b/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt @@ -12,3 +12,4 @@ char id=54 x=120 y=0 width=37 height=58 xoffset=4 yoffset=38 char id=55 x=36 y=59 width=33 height=57 xoffset=6 yoffset=39 xadvance=45 page=0 chnl=15 char id=56 x=0 y=0 width=40 height=58 xoffset=2 yoffset=38 xadvance=45 page=0 chnl=15 char id=57 x=81 y=0 width=38 height=58 xoffset=3 yoffset=38 xadvance=45 page=0 chnl=15 +char id=58 x=114 y=59 width=10 height=56 xoffset=0 yoffset=38 xadvance=10 page=0 chnl=15 \ No newline at end of file diff --git a/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png b/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png index c35991de..e1ff2fe8 100644 Binary files a/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png and b/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png differ diff --git a/resources-round-390x390/fonts/crystal-icons-extra-large.bak b/resources-round-390x390/fonts/crystal-icons-extra-large.bak new file mode 100644 index 00000000..1b0347a8 --- /dev/null +++ b/resources-round-390x390/fonts/crystal-icons-extra-large.bak @@ -0,0 +1,21 @@ +info face="Crystal Icons" size=-39 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0 +common lineHeight=39 base=39 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0 +page id=0 file="crystal-icons-extra-large.png" +chars count=17 +char id=48 x=0 y=0 width=34 height=39 xoffset=0 yoffset=0 xadvance=34 page=0 chnl=15 +char id=49 x=34 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=50 x=73 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=51 x=112 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=53 x=151 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=54 x=190 y=0 width=23 height=39 xoffset=8 yoffset=0 xadvance=39 page=0 chnl=15 +char id=55 x=213 y=0 width=35 height=39 xoffset=0 yoffset=0 xadvance=35 page=0 chnl=15 +char id=56 x=0 y=39 width=29 height=39 xoffset=0 yoffset=0 xadvance=29 page=0 chnl=15 +char id=57 x=30 y=39 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=58 x=69 y=39 width=35 height=39 xoffset=0 yoffset=0 xadvance=35 page=0 chnl=15 +char id=59 x=104 y=39 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=60 x=143 y=39 width=20 height=39 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15 +char id=61 x=163 y=39 width=9 height=9 xoffset=0 yoffset=13 xadvance=9 page=0 chnl=15 +char id=62 x=0 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15 +char id=63 x=45 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15 +char id=64 x=90 y=78 width=33 height=39 xoffset=0 yoffset=0 xadvance=33 page=0 chnl=15 +char id=65 x=123 y=78 width=27 height=39 xoffset=0 yoffset=0 xadvance=27 page=0 chnl=15 \ No newline at end of file diff --git a/resources-round-390x390/fonts/crystal-icons-extra-large.fnt b/resources-round-390x390/fonts/crystal-icons-extra-large.fnt index 1b0347a8..44129b8b 100644 --- a/resources-round-390x390/fonts/crystal-icons-extra-large.fnt +++ b/resources-round-390x390/fonts/crystal-icons-extra-large.fnt @@ -1,7 +1,7 @@ info face="Crystal Icons" size=-39 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0 common lineHeight=39 base=39 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0 page id=0 file="crystal-icons-extra-large.png" -chars count=17 +chars count=18 char id=48 x=0 y=0 width=34 height=39 xoffset=0 yoffset=0 xadvance=34 page=0 chnl=15 char id=49 x=34 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 char id=50 x=73 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 @@ -18,4 +18,5 @@ char id=61 x=163 y=39 width=9 height=9 xoffset=0 yoffset=13 char id=62 x=0 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15 char id=63 x=45 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15 char id=64 x=90 y=78 width=33 height=39 xoffset=0 yoffset=0 xadvance=33 page=0 chnl=15 -char id=65 x=123 y=78 width=27 height=39 xoffset=0 yoffset=0 xadvance=27 page=0 chnl=15 \ No newline at end of file +char id=65 x=123 y=78 width=27 height=39 xoffset=0 yoffset=0 xadvance=27 page=0 chnl=15 +char id=66 x=153 y=78 width=43 height=39 xoffset=0 yoffset=0 xadvance=43 page=0 chnl=15 \ No newline at end of file diff --git a/resources-round-390x390/fonts/crystal-icons-extra-large.png b/resources-round-390x390/fonts/crystal-icons-extra-large.png index 3a9378f3..e988b595 100644 Binary files a/resources-round-390x390/fonts/crystal-icons-extra-large.png and b/resources-round-390x390/fonts/crystal-icons-extra-large.png differ diff --git a/resources-round-390x390/fonts/fonts.xml b/resources-round-390x390/fonts/fonts.xml index 5421baeb..52d7c21e 100644 --- a/resources-round-390x390/fonts/fonts.xml +++ b/resources-round-390x390/fonts/fonts.xml @@ -1,32 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources-small-icons/fonts/crystal-icons-small.fnt b/resources-small-icons/fonts/crystal-icons-small.fnt index 148876d9..8668a3db 100644 --- a/resources-small-icons/fonts/crystal-icons-small.fnt +++ b/resources-small-icons/fonts/crystal-icons-small.fnt @@ -1,7 +1,7 @@ info face="Crystal Icons" size=-20 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0 common lineHeight=20 base=20 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0 page id=0 file="crystal-icons-small.png" -chars count=17 +chars count=18 char id=48 x=0 y=0 width=20 height=20 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15 char id=49 x=21 y=0 width=20 height=20 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15 char id=50 x=42 y=0 width=20 height=20 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15 @@ -18,4 +18,5 @@ char id=61 x=250 y=0 width=6 height=6 xoffset=0 yoffset=7 char id=62 x=0 y=21 width=23 height=20 xoffset=0 yoffset=0 xadvance=23 page=0 chnl=15 char id=63 x=24 y=21 width=23 height=20 xoffset=0 yoffset=0 xadvance=23 page=0 chnl=15 char id=64 x=48 y=21 width=17 height=20 xoffset=0 yoffset=0 xadvance=17 page=0 chnl=15 -char id=65 x=66 y=21 width=14 height=20 xoffset=0 yoffset=0 xadvance=14 page=0 chnl=15 \ No newline at end of file +char id=65 x=66 y=21 width=14 height=20 xoffset=0 yoffset=0 xadvance=14 page=0 chnl=15 +char id=66 x=82 y=23 width=20 height=18 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15 \ No newline at end of file diff --git a/resources-small-icons/fonts/crystal-icons-small.png b/resources-small-icons/fonts/crystal-icons-small.png index 351e80c2..5e3a0deb 100644 Binary files a/resources-small-icons/fonts/crystal-icons-small.png and b/resources-small-icons/fonts/crystal-icons-small.png differ diff --git a/resources/fonts/crystal-icons.fnt b/resources/fonts/crystal-icons.fnt index dd5b89a7..2b32067c 100644 --- a/resources/fonts/crystal-icons.fnt +++ b/resources/fonts/crystal-icons.fnt @@ -1,7 +1,7 @@ info face="Crystal Icons" size=-24 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0 common lineHeight=24 base=24 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0 page id=0 file="crystal-icons.png" -chars count=17 +chars count=18 char id=48 x=0 y=0 width=21 height=24 xoffset=1 yoffset=0 xadvance=24 page=0 chnl=15 char id=49 x=22 y=0 width=24 height=24 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15 char id=50 x=47 y=0 width=24 height=24 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15 @@ -18,4 +18,5 @@ char id=61 x=38 y=25 width=6 height=6 xoffset=0 yoffset=9 char id=62 x=47 y=25 width=28 height=24 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15 char id=63 x=76 y=25 width=28 height=24 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15 char id=64 x=105 y=25 width=20 height=24 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15 -char id=65 x=126 y=25 width=17 height=24 xoffset=0 yoffset=0 xadvance=17 page=0 chnl=15 \ No newline at end of file +char id=65 x=126 y=25 width=17 height=24 xoffset=0 yoffset=0 xadvance=17 page=0 chnl=15 +char id=66 x=145 y=27 width=24 height=22 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15 \ No newline at end of file diff --git a/resources/fonts/crystal-icons.png b/resources/fonts/crystal-icons.png index 9a7b658c..2dbdfae9 100644 Binary files a/resources/fonts/crystal-icons.png and b/resources/fonts/crystal-icons.png differ diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml index 3410e3fd..b1ccbd5b 100644 --- a/resources/settings/settings.xml +++ b/resources/settings/settings.xml @@ -88,6 +88,7 @@ @Strings.SunriseSunset @Strings.Weather @Strings.Humidity + @Strings.PulseOx @@ -107,6 +108,7 @@ @Strings.SunriseSunset @Strings.Weather @Strings.Humidity + @Strings.PulseOx @@ -126,6 +128,7 @@ @Strings.SunriseSunset @Strings.Weather @Strings.Humidity + @Strings.PulseOx diff --git a/resources/strings/strings.bak b/resources/strings/strings.bak new file mode 100644 index 00000000..f9512d17 --- /dev/null +++ b/resources/strings/strings.bak @@ -0,0 +1,104 @@ + + + Crystal + App Version + + Theme + Hours Colour + Minutes Colour + Left Meter + Right Meter + Calories Goal (1-10,000kCal) + Meter Style + Meter Digits Style + Add Local Time in City (Beta) + Move Bar Style + Hide Seconds + Hide Hours Leading Zero + + Number Of Data Fields + Data Field 1 + Data Field 2 + Data Field 3 + + Number Of Indicators + Indicator 1 + Indicator 2 + Indicator 3 + + (From Theme) + Mono Highlight + Mono + + All Segments (Merged) + Filled Segments (Merged) + All Segments + Filled Segments + Hidden + Current/Target + Current + + Blue (Dark) + Pink (Dark) + Red (Dark) + Green (Dark) + Cornflower Blue (Dark) + Lemon Cream (Dark) + Vivid Yellow (Dark) + Dayglo Orange (Dark) + Mono (Dark) + Mono (Light) + Blue (Light) + Red (Light) + Green (Light) + Dayglo Orange (Light) + Corn Yellow (Dark) + + Steps + Floors Climbed + Active Minutes (Weekly) + Calories (Manual Goal) + Off + + Heart Rate + Heart Rate (Live 5s) + Battery + Battery (Hide Percentage) + Notifications + Calories + Distance + Alarms + Altitude + Thermometer + Bluetooth + Bluetooth/Notifications + Sunrise/Sunset + Weather + Humidity + Pressure + + + + + Sun + Mon + Tue + Wed + Thu + Fri + Sat + + Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index f9512d17..05378315 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -76,6 +76,7 @@ Weather Humidity Pressure + Pulse Ox diff --git a/source/DataFields.bak b/source/DataFields.bak new file mode 100644 index 00000000..2f2cf216 --- /dev/null +++ b/source/DataFields.bak @@ -0,0 +1,730 @@ +using Toybox.WatchUi as Ui; +using Toybox.Graphics as Gfx; +using Toybox.System as Sys; +using Toybox.Application as App; +using Toybox.Activity as Activity; +using Toybox.ActivityMonitor as ActivityMonitor; +using Toybox.SensorHistory as SensorHistory; + +using Toybox.Time; +using Toybox.Time.Gregorian; + +enum /* FIELD_TYPES */ { + // Pseudo-fields. + FIELD_TYPE_SUNRISE = -1, + //FIELD_TYPE_SUNSET = -2, + + // Real fields (used by properties). + FIELD_TYPE_HEART_RATE = 0, + FIELD_TYPE_BATTERY, + FIELD_TYPE_NOTIFICATIONS, + FIELD_TYPE_CALORIES, + FIELD_TYPE_DISTANCE, + FIELD_TYPE_ALARMS, + FIELD_TYPE_ALTITUDE, + FIELD_TYPE_TEMPERATURE, + FIELD_TYPE_BATTERY_HIDE_PERCENT, + FIELD_TYPE_HR_LIVE_5S, + FIELD_TYPE_SUNRISE_SUNSET, + FIELD_TYPE_WEATHER, + FIELD_TYPE_PRESSURE, + FIELD_TYPE_HUMIDITY +} + +class DataFields extends Ui.Drawable { + + private var mLeft; + private var mRight; + private var mTop; + private var mBottom; + + private var mWeatherIconsFont; + private var mWeatherIconsSubset = null; // null, "d" for day subset, "n" for night subset. + + private var mFieldCount; + private var mHasLiveHR = false; // Is a live HR field currently being shown? + private var mWasHRAvailable = false; // HR availability at last full draw (in high power mode). + private var mMaxFieldLength; // Maximum number of characters per field. + private var mBatteryWidth; // Width of battery meter. + + // private const CM_PER_KM = 100000; + // private const MI_PER_KM = 0.621371; + // private const FT_PER_M = 3.28084; + + function initialize(params) { + Drawable.initialize(params); + + mLeft = params[:left]; + mRight = params[:right]; + mTop = params[:top]; + mBottom = params[:bottom]; + + mBatteryWidth = params[:batteryWidth]; + + // Initialise mFieldCount and mMaxFieldLength. + onSettingsChanged(); + } + + // Cache FieldCount setting, and determine appropriate maximum field length. + function onSettingsChanged() { + + // #123 Protect against null or unexpected type e.g. String. + mFieldCount = App.getApp().getIntProperty("FieldCount", 3); + + /* switch (mFieldCount) { + case 3: + mMaxFieldLength = 4; + break; + case 2: + mMaxFieldLength = 6; + break; + case 1: + mMaxFieldLength = 8; + break; + } */ + + // #116 Handle FieldCount = 0 correctly. + mMaxFieldLength = [0, 8, 6, 4][mFieldCount]; + + mHasLiveHR = App.getApp().hasField(FIELD_TYPE_HR_LIVE_5S); + + if (!App.getApp().hasField(FIELD_TYPE_WEATHER)) { + mWeatherIconsFont = null; + mWeatherIconsSubset = null; + } + } + + function draw(dc) { + update(dc, /* isPartialUpdate */ false); + } + + function update(dc, isPartialUpdate) { + if (isPartialUpdate && !mHasLiveHR) { + return; + } + + var fieldTypes = App.getApp().mFieldTypes; + + switch (mFieldCount) { + case 3: + drawDataField(dc, isPartialUpdate, fieldTypes[0], mLeft); + drawDataField(dc, isPartialUpdate, fieldTypes[1], (mRight + mLeft) / 2); + drawDataField(dc, isPartialUpdate, fieldTypes[2], mRight); + break; + case 2: + drawDataField(dc, isPartialUpdate, fieldTypes[0], mLeft + ((mRight - mLeft) * 0.15)); + drawDataField(dc, isPartialUpdate, fieldTypes[1], mLeft + ((mRight - mLeft) * 0.85)); + break; + case 1: + drawDataField(dc, isPartialUpdate, fieldTypes[0], (mRight + mLeft) / 2); + break; + /* + case 0: + break; + */ + } + } + + // Both regular and small icon fonts use same spot size for easier optimisation. + //private const LIVE_HR_SPOT_RADIUS = 3; + + private function drawDataField(dc, isPartialUpdate, fieldType, x) { + + // Assume we're only drawing live HR spot every 5 seconds; skip all other partial updates. + var isLiveHeartRate = (fieldType == FIELD_TYPE_HR_LIVE_5S); + var seconds = Sys.getClockTime().sec; + if (isPartialUpdate && (!isLiveHeartRate || (seconds % 5))) { + return; + } + + // Decide whether spot should be shown or not, based on current seconds. + var showLiveHRSpot = false; + var isHeartRate = ((fieldType == FIELD_TYPE_HEART_RATE) || isLiveHeartRate); + if (isHeartRate) { + + // High power mode: 0 on, 1 off, 2 on, etc. + if (!App.getApp().getView().isSleeping()) { + showLiveHRSpot = ((seconds % 2) == 0); + + // Low power mode: + } else { + + // Live HR: 0-4 on, 5-9 off, 10-14 on, etc. + if (isLiveHeartRate) { + showLiveHRSpot = (((seconds / 5) % 2) == 0); + + // Normal HR: turn off spot when entering sleep. + } else { + showLiveHRSpot = false; + } + } + } + + // 1. Value: draw first, as top of text overlaps icon. + var result = getValueForFieldType(fieldType); + var value = result["value"]; + + // Optimisation: if live HR remains unavailable, skip the rest of this partial update. + var isHRAvailable = isHeartRate && (value.length() != 0); + if (isPartialUpdate && !isHRAvailable && !mWasHRAvailable) { + return; + } + + // #34 Clip live HR value. + // Optimisation: hard-code clip rect dimensions. Possible, as all watches use same label font. + dc.setColor(gMonoLightColour, gBackgroundColour); + + if (isPartialUpdate) { + dc.setClip( + x - 11, + mBottom - 4, + 25, + 12); + + dc.clear(); + } + + dc.drawText( + x, + mBottom, + gNormalFont, + value, + Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER + ); + + // 2. Icon. + + // Grey out icon if no value was retrieved. + // #37 Do not grey out battery icon (getValueForFieldType() returns empty string). + var colour = (value.length() == 0) ? gMeterBackgroundColour : gThemeColour; + + // Battery. + if ((fieldType == FIELD_TYPE_BATTERY) || (fieldType == FIELD_TYPE_BATTERY_HIDE_PERCENT)) { + drawBatteryMeter(dc, x, mTop, mBatteryWidth, mBatteryWidth / 2); + + // #34 Live HR in low power mode. + } else if (isLiveHeartRate && isPartialUpdate) { + + // If HR availability changes while in low power mode, then we unfortunately have to draw the full heart. + // HR availability was recorded during the last high power draw cycle. + if (isHRAvailable != mWasHRAvailable) { + mWasHRAvailable = isHRAvailable; + + // Clip full heart, then draw. + var heartDims = dc.getTextDimensions("3", gIconsFont); // getIconFontCharForField(FIELD_TYPE_HR_LIVE_5S) + dc.setClip( + x - (heartDims[0] / 2), + mTop - (heartDims[1] / 2), + heartDims[0] + 1, + heartDims[1] + 1); + dc.setColor(colour, gBackgroundColour); + dc.drawText( + x, + mTop, + gIconsFont, + "3", // getIconFontCharForField(FIELD_TYPE_HR_LIVE_5S) + Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER + ); + } + + // Clip spot. + dc.setClip( + x - 3 /* LIVE_HR_SPOT_RADIUS */, + mTop - 3 /* LIVE_HR_SPOT_RADIUS */, + 7, // (2 * LIVE_HR_SPOT_RADIUS) + 1 + 7); // (2 * LIVE_HR_SPOT_RADIUS) + 1 + + // Draw spot, if it should be shown. + // fillCircle() does not anti-aliase, so use font instead. + var spotChar; + if (showLiveHRSpot && (Activity.getActivityInfo().currentHeartRate != null)) { + dc.setColor(gBackgroundColour, Graphics.COLOR_TRANSPARENT); + spotChar = "="; // getIconFontCharForField(LIVE_HR_SPOT) + + // Otherwise, fill in spot by drawing heart. + } else { + dc.setColor(colour, gBackgroundColour); + spotChar = "3"; // getIconFontCharForField(FIELD_TYPE_HR_LIVE_5S) + } + dc.drawText( + x, + mTop, + gIconsFont, + spotChar, + Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER + ); + + // Other icons. + } else { + + // #19 Show sunrise icon instead of default sunset icon, if sunrise is next. + if ((fieldType == FIELD_TYPE_SUNRISE_SUNSET) && (result["isSunriseNext"] == true)) { + fieldType = FIELD_TYPE_SUNRISE; + } + + var font; + var icon; + if (fieldType == FIELD_TYPE_WEATHER) { + + // #83 Dynamic loading/unloading of day/night weather icons font, to save memory. + // If subset has changed since last draw, save new subset, and load appropriate font for it. + var weatherIconsSubset = result["weatherIcon"].substring(2, 3); + if (!weatherIconsSubset.equals(mWeatherIconsSubset)) { + mWeatherIconsSubset = weatherIconsSubset; + mWeatherIconsFont = Ui.loadResource((mWeatherIconsSubset.equals("d")) ? + Rez.Fonts.WeatherIconsFontDay : Rez.Fonts.WeatherIconsFontNight); + } + font = mWeatherIconsFont; + + // #89 To avoid Unicode issues on real 735xt, rewrite char IDs as regular ASCII values, day icons starting from + // "A", night icons starting from "a" ("I" is shared). Also makes subsetting easier in fonts.xml. + // See https://openweathermap.org/weather-conditions. + icon = { + // Day icon Night icon Description + "01d" => "H" /* 61453 */, "01n" => "f" /* 61486 */, // clear sky + "02d" => "G" /* 61452 */, "02n" => "g" /* 61569 */, // few clouds + "03d" => "B" /* 61442 */, "03n" => "h" /* 61574 */, // scattered clouds + "04d" => "I" /* 61459 */, "04n" => "I" /* 61459 */, // broken clouds: day and night use same icon + "09d" => "E" /* 61449 */, "09n" => "d" /* 61481 */, // shower rain + "10d" => "D" /* 61448 */, "10n" => "c" /* 61480 */, // rain + "11d" => "C" /* 61445 */, "11n" => "b" /* 61477 */, // thunderstorm + "13d" => "F" /* 61450 */, "13n" => "e" /* 61482 */, // snow + "50d" => "A" /* 61441 */, "50n" => "a" /* 61475 */, // mist + }[result["weatherIcon"]]; + + } else { + font = gIconsFont; + + // Map fieldType to icon font char. + icon = { + FIELD_TYPE_SUNRISE => ">", + // FIELD_TYPE_SUNSET => "?", + + FIELD_TYPE_HEART_RATE => "3", + FIELD_TYPE_HR_LIVE_5S => "3", + // FIELD_TYPE_BATTERY => "4", + // FIELD_TYPE_BATTERY_HIDE_PERCENT => "4", + FIELD_TYPE_NOTIFICATIONS => "5", + FIELD_TYPE_CALORIES => "6", + FIELD_TYPE_DISTANCE => "7", + FIELD_TYPE_ALARMS => ":", + FIELD_TYPE_ALTITUDE => ";", + FIELD_TYPE_TEMPERATURE => "<", + // FIELD_TYPE_WEATHER => "<", + // LIVE_HR_SPOT => "=", + + FIELD_TYPE_SUNRISE_SUNSET => "?", + FIELD_TYPE_PRESSURE => "@", + FIELD_TYPE_HUMIDITY => "A", + }[fieldType]; + } + + dc.setColor(colour, gBackgroundColour); + dc.drawText( + x, + mTop, + font, + icon, + Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER + ); + + if (isHeartRate) { + + // #34 Save whether HR was available during this high power draw cycle. + mWasHRAvailable = isHRAvailable; + + // #34 Live HR in high power mode. + if (showLiveHRSpot && (Activity.getActivityInfo().currentHeartRate != null)) { + dc.setColor(gBackgroundColour, Graphics.COLOR_TRANSPARENT); + dc.drawText( + x, + mTop, + gIconsFont, + "=", // getIconFontCharForField(LIVE_HR_SPOT) + Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER + ); + } + } + } + } + + // Return empty result["value"] string if value cannot be retrieved (e.g. unavailable, or unsupported). + // result["isSunriseNext"] indicates that sunrise icon should be shown for FIELD_TYPE_SUNRISE_SUNSET, rather than default + // sunset icon. + private function getValueForFieldType(type) { + var result = {}; + var value = ""; + + var settings = Sys.getDeviceSettings(); + + var activityInfo; + var sample; + var altitude; + var pressure = null; // May never be initialised if no support for pressure (CIQ 1.x devices). + var temperature; + var weather; + var weatherValue; + var sunTimes; + var unit; + + switch (type) { + case FIELD_TYPE_HEART_RATE: + case FIELD_TYPE_HR_LIVE_5S: + // #34 Try to retrieve live HR from Activity::Info, before falling back to historical HR from ActivityMonitor. + activityInfo = Activity.getActivityInfo(); + sample = activityInfo.currentHeartRate; + if (sample != null) { + value = sample.format(INTEGER_FORMAT); + } else if (ActivityMonitor has :getHeartRateHistory) { + sample = ActivityMonitor.getHeartRateHistory(1, /* newestFirst */ true) + .next(); + if ((sample != null) && (sample.heartRate != ActivityMonitor.INVALID_HR_SAMPLE)) { + value = sample.heartRate.format(INTEGER_FORMAT); + } + } + break; + + case FIELD_TYPE_BATTERY: + // #8: battery returned as float. Use floor() to match native. Must match drawBatteryMeter(). + value = Math.floor(Sys.getSystemStats().battery); + value = value.format(INTEGER_FORMAT) + "%"; + break; + + // #37 Return empty string. updateDataField() has special case so that battery icon is not greyed out. + // case FIELD_TYPE_BATTERY_HIDE_PERCENT: + // break; + + case FIELD_TYPE_NOTIFICATIONS: + if (settings.notificationCount > 0) { + value = settings.notificationCount.format(INTEGER_FORMAT); + } + break; + + case FIELD_TYPE_CALORIES: + activityInfo = ActivityMonitor.getInfo(); + value = activityInfo.calories.format(INTEGER_FORMAT); + break; + + case FIELD_TYPE_DISTANCE: + activityInfo = ActivityMonitor.getInfo(); + value = activityInfo.distance.toFloat() / /* CM_PER_KM */ 100000; // #11: Ensure floating point division! + + if (settings.distanceUnits == System.UNIT_METRIC) { + unit = "km"; + } else { + value *= /* MI_PER_KM */ 0.621371; + unit = "mi"; + } + + value = value.format("%.1f"); + + // Show unit only if value plus unit fits within maximum field length. + if ((value.length() + unit.length()) <= mMaxFieldLength) { + value += unit; + } + + break; + + case FIELD_TYPE_ALARMS: + if (settings.alarmCount > 0) { + value = settings.alarmCount.format(INTEGER_FORMAT); + } + break; + + case FIELD_TYPE_ALTITUDE: + // #67 Try to retrieve altitude from current activity, before falling back on elevation history. + // Note that Activity::Info.altitude is supported by CIQ 1.x, but elevation history only on select CIQ 2.x + // devices. + activityInfo = Activity.getActivityInfo(); + altitude = activityInfo.altitude; + if ((altitude == null) && (Toybox has :SensorHistory) && (Toybox.SensorHistory has :getElevationHistory)) { + sample = SensorHistory.getElevationHistory({ :period => 1, :order => SensorHistory.ORDER_NEWEST_FIRST }) + .next(); + if ((sample != null) && (sample.data != null)) { + altitude = sample.data; + } + } + if (altitude != null) { + + // Metres (no conversion necessary). + if (settings.elevationUnits == System.UNIT_METRIC) { + unit = "m"; + + // Feet. + } else { + altitude *= /* FT_PER_M */ 3.28084; + unit = "ft"; + } + + value = altitude.format(INTEGER_FORMAT); + + // Show unit only if value plus unit fits within maximum field length. + if ((value.length() + unit.length()) <= mMaxFieldLength) { + value += unit; + } + } + break; + + case FIELD_TYPE_TEMPERATURE: + if ((Toybox has :SensorHistory) && (Toybox.SensorHistory has :getTemperatureHistory)) { + sample = SensorHistory.getTemperatureHistory(null).next(); + if ((sample != null) && (sample.data != null)) { + temperature = sample.data; + + if (settings.temperatureUnits == System.UNIT_STATUTE) { + temperature = (temperature * (9.0 / 5)) + 32; // Convert to Farenheit: ensure floating point division. + } + + value = temperature.format(INTEGER_FORMAT) + "°"; + } + } + break; + + case FIELD_TYPE_SUNRISE_SUNSET: + + if (gLocationLat != null) { + var nextSunEvent = 0; + var now = Gregorian.info(Time.now(), Time.FORMAT_SHORT); + + // Convert to same format as sunTimes, for easier comparison. Add a minute, so that e.g. if sun rises at + // 07:38:17, then 07:38 is already consided daytime (seconds not shown to user). + now = now.hour + ((now.min + 1) / 60.0); + //Sys.println(now); + + // Get today's sunrise/sunset times in current time zone. + sunTimes = getSunTimes(gLocationLat, gLocationLng, null, /* tomorrow */ false); + //Sys.println(sunTimes); + + // If sunrise/sunset happens today. + var sunriseSunsetToday = ((sunTimes[0] != null) && (sunTimes[1] != null)); + if (sunriseSunsetToday) { + + // Before sunrise today: today's sunrise is next. + if (now < sunTimes[0]) { + nextSunEvent = sunTimes[0]; + result["isSunriseNext"] = true; + + // After sunrise today, before sunset today: today's sunset is next. + } else if (now < sunTimes[1]) { + nextSunEvent = sunTimes[1]; + + // After sunset today: tomorrow's sunrise (if any) is next. + } else { + sunTimes = getSunTimes(gLocationLat, gLocationLng, null, /* tomorrow */ true); + nextSunEvent = sunTimes[0]; + result["isSunriseNext"] = true; + } + } + + // Sun never rises/sets today. + if (!sunriseSunsetToday) { + value = "---"; + + // Sun never rises: sunrise is next, but more than a day from now. + if (sunTimes[0] == null) { + result["isSunriseNext"] = true; + } + + // We have a sunrise/sunset time. + } else { + var hour = Math.floor(nextSunEvent).toLong() % 24; + var min = Math.floor((nextSunEvent - Math.floor(nextSunEvent)) * 60); // Math.floor(fractional_part * 60) + value = App.getApp().getFormattedTime(hour, min); + value = value[:hour] + ":" + value[:min] + value[:amPm]; + } + + // Waiting for location. + } else { + value = "gps?"; + } + + break; + + case FIELD_TYPE_WEATHER: + case FIELD_TYPE_HUMIDITY: + + // Default = sunshine! + if (type == FIELD_TYPE_WEATHER) { + result["weatherIcon"] = "01d"; + } + + weather = App.getApp().getProperty("OpenWeatherMapCurrent"); + + // Awaiting location. + if (gLocationLat == null) { + value = "gps?"; + + // Stored weather data available. + } else if (weather != null) { + + // FIELD_TYPE_WEATHER. + if (type == FIELD_TYPE_WEATHER) { + weatherValue = weather["temp"]; // Celcius. + + if (settings.temperatureUnits == System.UNIT_STATUTE) { + weatherValue = (weatherValue * (9.0 / 5)) + 32; // Convert to Farenheit: ensure floating point division. + } + + value = weatherValue.format(INTEGER_FORMAT) + "°"; + result["weatherIcon"] = weather["icon"]; + + // FIELD_TYPE_HUMIDITY. + } else { + weatherValue = weather["humidity"]; + value = weatherValue.format(INTEGER_FORMAT) + "%"; + } + + // Awaiting response. + } else if ((App.getApp().getProperty("PendingWebRequests") != null) && + App.getApp().getProperty("PendingWebRequests")["OpenWeatherMapCurrent"]) { + + value = "..."; + } + break; + + case FIELD_TYPE_PRESSURE: + + // Avoid using ActivityInfo.ambientPressure, as this bypasses any manual pressure calibration e.g. on Fenix + // 5. Pressure is unlikely to change frequently, so there isn't the same concern with getting a "live" value, + // compared with HR. Use SensorHistory only. + if ((Toybox has :SensorHistory) && (Toybox.SensorHistory has :getPressureHistory)) { + sample = SensorHistory.getPressureHistory(null).next(); + if ((sample != null) && (sample.data != null)) { + pressure = sample.data; + } + } + + if (pressure != null) { + unit = "mb"; + pressure = pressure / 100; // Pa --> mbar; + value = pressure.format("%.1f"); + + // If single decimal place doesn't fit, truncate to integer. + if (value.length() > mMaxFieldLength) { + value = pressure.format(INTEGER_FORMAT); + + // Otherwise, if unit fits as well, add it. + } else if (value.length() + unit.length() <= mMaxFieldLength) { + value = value + unit; + } + } + break; + } + + result["value"] = value; + return result; + } + + /** + * With thanks to ruiokada. Adapted, then translated to Monkey C, from: + * https://gist.github.com/ruiokada/b28076d4911820ddcbbc + * + * Calculates sunrise and sunset in local time given latitude, longitude, and tz. + * + * Equations taken from: + * https://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_Day_Number + * https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth + * + * @method getSunTimes + * @param {Float} lat Latitude of location (South is negative) + * @param {Float} lng Longitude of location (West is negative) + * @param {Integer || null} tz Timezone hour offset. e.g. Pacific/Los Angeles is -8 (Specify null for system timezone) + * @param {Boolean} tomorrow Calculate tomorrow's sunrise and sunset, instead of today's. + * @return {Array} Returns array of length 2 with sunrise and sunset as floats. + * Returns array with [null, -1] if the sun never rises, and [-1, null] if the sun never sets. + */ + private function getSunTimes(lat, lng, tz, tomorrow) { + + // Use double precision where possible, as floating point errors can affect result by minutes. + lat = lat.toDouble(); + lng = lng.toDouble(); + + var now = Time.now(); + if (tomorrow) { + now = now.add(new Time.Duration(24 * 60 * 60)); + } + var d = Gregorian.info(Time.now(), Time.FORMAT_SHORT); + var rad = Math.PI / 180.0d; + var deg = 180.0d / Math.PI; + + // Calculate Julian date from Gregorian. + var a = Math.floor((14 - d.month) / 12); + var y = d.year + 4800 - a; + var m = d.month + (12 * a) - 3; + var jDate = d.day + + Math.floor(((153 * m) + 2) / 5) + + (365 * y) + + Math.floor(y / 4) + - Math.floor(y / 100) + + Math.floor(y / 400) + - 32045; + + // Number of days since Jan 1st, 2000 12:00. + var n = jDate - 2451545.0d + 0.0008d; + //Sys.println("n " + n); + + // Mean solar noon. + var jStar = n - (lng / 360.0d); + //Sys.println("jStar " + jStar); + + // Solar mean anomaly. + var M = 357.5291d + (0.98560028d * jStar); + var MFloor = Math.floor(M); + var MFrac = M - MFloor; + M = MFloor.toLong() % 360; + M += MFrac; + //Sys.println("M " + M); + + // Equation of the centre. + var C = 1.9148d * Math.sin(M * rad) + + 0.02d * Math.sin(2 * M * rad) + + 0.0003d * Math.sin(3 * M * rad); + //Sys.println("C " + C); + + // Ecliptic longitude. + var lambda = (M + C + 180 + 102.9372d); + var lambdaFloor = Math.floor(lambda); + var lambdaFrac = lambda - lambdaFloor; + lambda = lambdaFloor.toLong() % 360; + lambda += lambdaFrac; + //Sys.println("lambda " + lambda); + + // Solar transit. + var jTransit = 2451545.5d + jStar + + 0.0053d * Math.sin(M * rad) + - 0.0069d * Math.sin(2 * lambda * rad); + //Sys.println("jTransit " + jTransit); + + // Declination of the sun. + var delta = Math.asin(Math.sin(lambda * rad) * Math.sin(23.44d * rad)); + //Sys.println("delta " + delta); + + // Hour angle. + var cosOmega = (Math.sin(-0.83d * rad) - Math.sin(lat * rad) * Math.sin(delta)) + / (Math.cos(lat * rad) * Math.cos(delta)); + //Sys.println("cosOmega " + cosOmega); + + // Sun never rises. + if (cosOmega > 1) { + return [null, -1]; + } + + // Sun never sets. + if (cosOmega < -1) { + return [-1, null]; + } + + // Calculate times from omega. + var omega = Math.acos(cosOmega) * deg; + var jSet = jTransit + (omega / 360.0); + var jRise = jTransit - (omega / 360.0); + var deltaJSet = jSet - jDate; + var deltaJRise = jRise - jDate; + + var tzOffset = (tz == null) ? (Sys.getClockTime().timeZoneOffset / 3600) : tz; + return [ + /* localRise */ (deltaJRise * 24) + tzOffset, + /* localSet */ (deltaJSet * 24) + tzOffset + ]; + } +} diff --git a/source/DataFields.mc b/source/DataFields.mc index 2f2cf216..89b525a8 100644 --- a/source/DataFields.mc +++ b/source/DataFields.mc @@ -28,7 +28,8 @@ enum /* FIELD_TYPES */ { FIELD_TYPE_SUNRISE_SUNSET, FIELD_TYPE_WEATHER, FIELD_TYPE_PRESSURE, - FIELD_TYPE_HUMIDITY + FIELD_TYPE_HUMIDITY, + FIELD_TYPE_PULSE_OX } class DataFields extends Ui.Drawable { @@ -316,6 +317,7 @@ class DataFields extends Ui.Drawable { FIELD_TYPE_SUNRISE_SUNSET => "?", FIELD_TYPE_PRESSURE => "@", FIELD_TYPE_HUMIDITY => "A", + FIELD_TYPE_PULSE_OX => "B", // SG Addition }[fieldType]; } @@ -368,6 +370,14 @@ class DataFields extends Ui.Drawable { var unit; switch (type) { + // SG Addition + case FIELD_TYPE_PULSE_OX: + activityInfo = Activity.getActivityInfo(); + sample = activityInfo != null and activityInfo has :currentOxygenSaturation ? activityInfo.currentOxygenSaturation : null; + if (sample != null) { + value = sample.format(INTEGER_FORMAT); + } + break; case FIELD_TYPE_HEART_RATE: case FIELD_TYPE_HR_LIVE_5S: // #34 Try to retrieve live HR from Activity::Info, before falling back to historical HR from ActivityMonitor.