Add the on-device data field catalogue, always-on mode, and real icons
Fields 75 selectable fields across date/time, activity, body, system, environment, weather and custom text, numbered to match watchface.io/docs/datafields so the two catalogues line up. All four slots (both top, both bottom) pick from the same table and the bottom captions follow the selection. tools/gen_fields.py owns the table and emits FieldTable.mc, settings.xml and both languages' strings, so those cannot drift apart. Fields.Ctx fetches each API at most once per draw, and only if some slot asks -- the forecast, user profile and sensor history lookups are lazy. The rest of that catalogue is deliberately absent: OpenWeatherMap and StormGlass need network calls and user API keys, the per-sport weekly and 28-day aggregates come from a phone-side Garmin Connect integration rather than any on-device API, and the app complications need those apps installed. README section 7 lists what was left out and why. Permissions UserProfile and SensorHistory are now declared; without them resting heart rate, VO2 max, pulse ox and body battery throw at runtime. Sensor turns out to be rejected outright for type="watchface", so altitude and pressure read from Activity.Info instead. Always-on display AMOLED devices need the lit-pixel count cut while asleep. drawAmbient drops the band fill and the unlit ticks, dims what remains, hides the complications, and walks the layout within +/-4 px per minute so nothing burns in. Gated on requiresBurnInProtection, so MIP devices are untouched. Icons The comps' sunrise and weather icons have two- and three-pixel features. Rebuilding them from circles and lines looked right at 454 px and turned to mush at 280. They are now rasterised from the design paths once per screen width and tinted with drawBitmap2(:tintColor) -- which needs packingFormat="png", since a palettised source is refused at runtime. Launcher icons are generated at the five sizes the devices ask for. All 17 devices build; checked on fenix847mm and enduro3 in the simulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import Toybox.Time;
|
||||
import Toybox.Time.Gregorian;
|
||||
import Toybox.WatchUi;
|
||||
import Themes;
|
||||
import Fields;
|
||||
import Toybox.SensorHistory;
|
||||
|
||||
// Every literal below is a coordinate on the 500x500 design canvas, measured
|
||||
// from design/watchface-*.svg. Nothing here is eyeballed: ring geometry comes
|
||||
@@ -47,7 +49,7 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
|
||||
const LEFT_X = 157.00; // top complications, centred
|
||||
const RIGHT_X = 343.70;
|
||||
const ICON_CY = 90.00;
|
||||
const ICON_CY = 89.50;
|
||||
const VALUE_CY = 131.06;
|
||||
|
||||
const TIME_CY = 221.10;
|
||||
@@ -89,22 +91,37 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
private var mFontBand = Graphics.FONT_SMALL;
|
||||
private var mFontComp = Graphics.FONT_MEDIUM;
|
||||
private var mFontTime = Graphics.FONT_NUMBER_HOT;
|
||||
private var mDow = null;
|
||||
private var mMon = null;
|
||||
private var mLabels = null;
|
||||
private var mMeridiem = null;
|
||||
private var mDateFmt = " ";
|
||||
// Always-on display: AMOLED devices must cut lit pixels hard while
|
||||
// asleep, and shift what is left so nothing burns in.
|
||||
private var mSleeping = false;
|
||||
private var mBurnIn = false;
|
||||
private var mIconSunrise = null;
|
||||
private var mIconSunset = null;
|
||||
private var mIconWeather = null;
|
||||
|
||||
function initialize() {
|
||||
WatchFace.initialize();
|
||||
}
|
||||
|
||||
function onEnterSleep() as Void {
|
||||
mSleeping = true;
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function onExitSleep() as Void {
|
||||
mSleeping = false;
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function onLayout(dc as Dc) as Void {
|
||||
mW = dc.getWidth();
|
||||
mH = dc.getHeight();
|
||||
mS = mW / 500.0;
|
||||
mCx = mW / 2;
|
||||
mCy = mH / 2;
|
||||
var ds = System.getDeviceSettings();
|
||||
mBurnIn = (ds != null) && (ds has :requiresBurnInProtection)
|
||||
&& ds.requiresBurnInProtection;
|
||||
var text = [Graphics.FONT_XTINY, Graphics.FONT_TINY, Graphics.FONT_SMALL,
|
||||
Graphics.FONT_MEDIUM, Graphics.FONT_LARGE];
|
||||
var nums = [Graphics.FONT_NUMBER_MILD, Graphics.FONT_NUMBER_MEDIUM,
|
||||
@@ -115,28 +132,11 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
loadStrings();
|
||||
}
|
||||
|
||||
// Every piece of on-face wording lives in resources so resources-chn can
|
||||
// replace it. loadResource is not cheap, so pull it all once per layout.
|
||||
function loadStrings() as Void {
|
||||
mDow = splitCsv(WatchUi.loadResource(Rez.Strings.DowNames) as String);
|
||||
mMon = splitCsv(WatchUi.loadResource(Rez.Strings.MonNames) as String);
|
||||
mLabels = splitCsv(WatchUi.loadResource(Rez.Strings.FieldLabels) as String);
|
||||
mMeridiem = splitCsv(WatchUi.loadResource(Rez.Strings.Meridiem) as String);
|
||||
mDateFmt = WatchUi.loadResource(Rez.Strings.DateFormat) as String;
|
||||
}
|
||||
|
||||
// Monkey C has no String.split.
|
||||
function splitCsv(str as String) as Array<String> {
|
||||
var out = [] as Array<String>;
|
||||
var rest = str;
|
||||
var cut = rest.find(",");
|
||||
while (cut != null) {
|
||||
out.add(rest.substring(0, cut) as String);
|
||||
rest = rest.substring(cut + 1, rest.length()) as String;
|
||||
cut = rest.find(",");
|
||||
}
|
||||
out.add(rest);
|
||||
return out;
|
||||
Fields.loadStrings();
|
||||
mIconSunrise = WatchUi.loadResource(Rez.Drawables.IconSunrise);
|
||||
mIconSunset = WatchUi.loadResource(Rez.Drawables.IconSunset);
|
||||
mIconWeather = WatchUi.loadResource(Rez.Drawables.IconWeather);
|
||||
}
|
||||
|
||||
// Connect IQ exposes ascent and descent but not cap height, and the ascent
|
||||
@@ -254,6 +254,15 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
? info.floorsClimbedGoal.toFloat() : 10.0f;
|
||||
p = info.floorsClimbed.toFloat() / goal;
|
||||
}
|
||||
} else if (idx == 5) { // Active minutes vs the weekly goal
|
||||
if (info.activeMinutesWeek != null && info.activeMinutesWeekGoal != null
|
||||
&& info.activeMinutesWeekGoal > 0) {
|
||||
p = info.activeMinutesWeek.total.toFloat()
|
||||
/ info.activeMinutesWeekGoal.toFloat();
|
||||
}
|
||||
} else if (idx == 6) { // Body battery is already 0..100
|
||||
var bb = Fields.latest(SensorHistory.getBodyBatteryHistory({:period => 1}));
|
||||
if (bb != null) { p = bb.toFloat() / 100.0; }
|
||||
} else if (idx == 4) { // Move bar: an integer level, not a ratio
|
||||
if (info.moveBarLevel != null) {
|
||||
var span = (ActivityMonitor.MOVE_BAR_LEVEL_MAX
|
||||
@@ -268,103 +277,6 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
return p;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ data bits
|
||||
function distanceString(info as ActivityMonitor.Info, ds as System.DeviceSettings) as String {
|
||||
if (info == null || info.distance == null) { return "0.0"; }
|
||||
var km = info.distance.toFloat() / 100000.0;
|
||||
if (ds != null && ds.distanceUnits == System.UNIT_STATUTE) { km = km * 0.621371; }
|
||||
return km.format("%.1f");
|
||||
}
|
||||
|
||||
// TempUnit: 0 follow the watch, 1 force Celsius, 2 force Fahrenheit.
|
||||
function tempString(c as Numeric?, ds as System.DeviceSettings) as String {
|
||||
if (c == null) { return "--"; }
|
||||
var t = c.toFloat();
|
||||
var mode = getSetting("TempUnit", 0);
|
||||
var fahrenheit = (mode == 2)
|
||||
|| (mode == 0 && ds != null && ds.temperatureUnits == System.UNIT_STATUTE);
|
||||
if (fahrenheit) { t = t * 9.0 / 5.0 + 32.0; }
|
||||
return t.format("%d") + "°";
|
||||
}
|
||||
|
||||
function clockString(m as Time.Moment?, ds as System.DeviceSettings) as String {
|
||||
if (m == null) { return "--:--"; }
|
||||
var g = Gregorian.info(m, Time.FORMAT_SHORT);
|
||||
var h = g.hour;
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
return h.format("%d") + ":" + g.min.format("%02d");
|
||||
}
|
||||
|
||||
function here(weather as Weather.CurrentConditions?) as Position.Location? {
|
||||
if (weather != null && weather.observationLocationPosition != null) {
|
||||
return weather.observationLocationPosition;
|
||||
}
|
||||
var act = Activity.getActivityInfo();
|
||||
if (act != null && act.currentLocation != null) { return act.currentLocation; }
|
||||
return null;
|
||||
}
|
||||
|
||||
// value string for a complication slot
|
||||
function complicationValue(typeIdx as Number, info as ActivityMonitor.Info,
|
||||
weather as Weather.CurrentConditions?, stats as System.Stats,
|
||||
ds as System.DeviceSettings) as String {
|
||||
if (typeIdx == 1) { // Calories
|
||||
return info != null && info.calories != null ? info.calories.format("%d") : "0";
|
||||
} else if (typeIdx == 2) { // Steps
|
||||
return info != null && info.steps != null ? info.steps.format("%d") : "0";
|
||||
} else if (typeIdx == 3) { // Distance
|
||||
return distanceString(info, ds);
|
||||
} else if (typeIdx == 4) { // Floors
|
||||
return info != null && info.floorsClimbed != null
|
||||
? info.floorsClimbed.format("%d") : "0";
|
||||
} else if (typeIdx == 5) { // Battery
|
||||
var pct = 0;
|
||||
if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
|
||||
return pct.format("%d") + "%";
|
||||
} else if (typeIdx == 6 || typeIdx == 7) { // Sunrise / sunset
|
||||
var loc = here(weather);
|
||||
if (loc == null) { return "--:--"; }
|
||||
var m = typeIdx == 6 ? Weather.getSunrise(loc, Time.now())
|
||||
: Weather.getSunset(loc, Time.now());
|
||||
return clockString(m, ds);
|
||||
} else if (typeIdx == 9) { // Heart rate
|
||||
var act = Activity.getActivityInfo();
|
||||
if (act != null && act.currentHeartRate != null) {
|
||||
return act.currentHeartRate.format("%d");
|
||||
}
|
||||
var hist = ActivityMonitor.getHeartRateHistory(1, true);
|
||||
if (hist != null) {
|
||||
var sample = hist.next();
|
||||
if (sample != null && sample.heartRate != null
|
||||
&& sample.heartRate != ActivityMonitor.INVALID_HR_SAMPLE) {
|
||||
return sample.heartRate.format("%d");
|
||||
}
|
||||
}
|
||||
return "--";
|
||||
} else if (typeIdx == 8) { // Forecast high / low
|
||||
var fc = Weather.getDailyForecast();
|
||||
if (fc != null && fc.size() > 0) {
|
||||
return tempString(fc[0].highTemperature, ds) + "/"
|
||||
+ tempString(fc[0].lowTemperature, ds);
|
||||
}
|
||||
return "--/--";
|
||||
}
|
||||
// 0: current temperature
|
||||
if (weather != null && weather.temperature != null) {
|
||||
return tempString(weather.temperature, ds);
|
||||
}
|
||||
return "--";
|
||||
}
|
||||
|
||||
// Short caption under a bottom-row value. Indexed like the field types, so
|
||||
// the label always follows whatever the slot is set to.
|
||||
function fieldLabel(typeIdx as Number) as String {
|
||||
if (mLabels == null || typeIdx < 0 || typeIdx >= mLabels.size()) { return ""; }
|
||||
return mLabels[typeIdx];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- ring
|
||||
function tick(dc as Dc, ang as Float, hw as Float) as Void {
|
||||
var a0 = ang - hw;
|
||||
@@ -441,79 +353,19 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
}
|
||||
}
|
||||
|
||||
// Filled half-ellipse, flat side down. The comp's dome is 14.90 wide but
|
||||
// 8.93 tall, so it is not a plain semicircle.
|
||||
function dome(dc as Dc, cx as Numeric, baseY as Numeric,
|
||||
rx as Numeric, ry as Numeric) as Void {
|
||||
var pts = [];
|
||||
for (var i = 0; i <= 12; i++) {
|
||||
var rad = Math.toRadians(180.0 - 15.0 * i);
|
||||
pts.add([Math.round(px(cx) + Math.cos(rad) * px(rx)).toNumber(),
|
||||
Math.round(px(baseY) - Math.sin(rad) * px(ry)).toNumber()]);
|
||||
}
|
||||
dc.fillPolygon(pts);
|
||||
}
|
||||
|
||||
function stub(dc as Dc, x1 as Numeric, y1 as Numeric, x2 as Numeric, y2 as Numeric) as Void {
|
||||
dc.drawLine(px(x1), px(y1), px(x2), px(y2));
|
||||
}
|
||||
|
||||
// Sun over a horizon bar with a chevron. Every ray in the comp is a short
|
||||
// stub roughly as long as the pen is wide -- drawing them longer is what
|
||||
// made this icon read taller than the weather one.
|
||||
// dir = -1 flips the chevron up, which is what the sunset slot uses.
|
||||
function iconSun(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
|
||||
col as Number, dir as Number) as Void {
|
||||
var k = scale;
|
||||
var ox = cx - 156.32 * k; // design-space offset of this icon
|
||||
var oy = cy - 90.34 * k;
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dome(dc, ox + 156.39 * k, oy + 94.02 * k, 7.45 * k, 8.93 * k);
|
||||
dc.setPenWidth(maxNum(2, px(2.96 * k)));
|
||||
stub(dc, ox + 156.39 * k, oy + 79.4 * k, ox + 156.39 * k, oy + 81.1 * k);
|
||||
stub(dc, ox + 148.2 * k, oy + 83.3 * k, ox + 146.5 * k, oy + 84.5 * k);
|
||||
stub(dc, ox + 164.6 * k, oy + 84.5 * k, ox + 166.3 * k, oy + 83.3 * k);
|
||||
stub(dc, ox + 142.6 * k, oy + 92.5 * k, ox + 144.7 * k, oy + 92.5 * k);
|
||||
stub(dc, ox + 168.0 * k, oy + 92.5 * k, ox + 170.1 * k, oy + 92.5 * k);
|
||||
dc.setPenWidth(maxNum(2, px(2.6 * k)));
|
||||
var by = oy + 98.45 * k;
|
||||
stub(dc, ox + 142.3 * k, by, ox + 170.3 * k, by);
|
||||
stub(dc, ox + 152.6 * k, by, ox + 156.3 * k, by + dir * 3.05 * k);
|
||||
stub(dc, ox + 156.3 * k, by + dir * 3.05 * k, ox + 160.0 * k, by);
|
||||
}
|
||||
|
||||
// Sun peeking out from behind a cloud. The comp keeps a dark notch where
|
||||
// the cloud crosses the sun; without it the two shapes merge into one blob
|
||||
// and the icon reads much heavier than the sunrise one next to it. So the
|
||||
// cloud is stamped twice: once inflated in black to cut the notch, then at
|
||||
// true size in the accent colour.
|
||||
function cloudBody(dc as Dc, ox as Numeric, oy as Numeric, k as Float,
|
||||
grow as Float) as Void {
|
||||
dc.fillCircle(px(ox + 336.6 * k), px(oy + 94.4 * k), px(8.7 * k + grow));
|
||||
dc.fillCircle(px(ox + 346.4 * k), px(oy + 96.9 * k), px(6.3 * k + grow));
|
||||
var g = px(grow);
|
||||
dc.fillRoundedRectangle(px(ox + 328.0 * k) - g, px(oy + 95.5 * k) - g,
|
||||
px(24.6 * k) + 2 * g, px(7.75 * k) + g,
|
||||
px(3.4 * k));
|
||||
}
|
||||
|
||||
function iconCloud(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
|
||||
col as Number) as Void {
|
||||
var k = scale;
|
||||
var ox = cx - 343.55 * k;
|
||||
var oy = cy - 89.25 * k;
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillCircle(px(ox + 347.0 * k), px(oy + 88.9 * k), px(7.8 * k));
|
||||
dc.setPenWidth(maxNum(2, px(2.3 * k)));
|
||||
stub(dc, ox + 346.2 * k, oy + 76.0 * k, ox + 346.2 * k, oy + 78.7 * k);
|
||||
stub(dc, ox + 354.9 * k, oy + 87.7 * k, ox + 357.5 * k, oy + 87.7 * k);
|
||||
dc.setPenWidth(maxNum(2, px(2.7 * k)));
|
||||
stub(dc, ox + 339.5 * k, oy + 79.5 * k, ox + 338.1 * k, oy + 80.9 * k);
|
||||
stub(dc, ox + 353.5 * k, oy + 80.9 * k, ox + 354.6 * k, oy + 79.8 * k);
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
|
||||
cloudBody(dc, ox, oy, k, 1.7 * k);
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
cloudBody(dc, ox, oy, k, 0.0);
|
||||
// The comps' sunrise and weather icons are traced outlines with two- and
|
||||
// three-pixel features. Rebuilding them from circles and lines held up at
|
||||
// 454 px and turned to mush at 280, so they ship as bitmaps rasterised
|
||||
// from the design paths at each screen size (tools/gen_icons.py) and get
|
||||
// the theme's accent applied through :tintColor.
|
||||
function blitIcon(dc as Dc, bmp, cx as Numeric, cy as Numeric, col as Number) as Void {
|
||||
if (bmp == null) { return; }
|
||||
dc.drawBitmap2(px(cx) - bmp.getWidth() / 2, px(cy) - bmp.getHeight() / 2,
|
||||
bmp, {:tintColor => col});
|
||||
}
|
||||
|
||||
function maxNum(a as Number, b as Number) as Number {
|
||||
@@ -522,47 +374,52 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
|
||||
function drawComplicationIcon(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
|
||||
col as Number, typeIdx as Number, level as Float) as Void {
|
||||
if (typeIdx == 6) {
|
||||
iconSun(dc, cx, cy, scale, col, 1);
|
||||
} else if (typeIdx == 7) {
|
||||
iconSun(dc, cx, cy, scale, col, -1);
|
||||
} else if (typeIdx == 0 || typeIdx == 8) {
|
||||
iconCloud(dc, cx, cy, scale, col);
|
||||
} else if (typeIdx == 5) {
|
||||
if (typeIdx == 712 || typeIdx == 700) {
|
||||
blitIcon(dc, mIconSunrise, cx, ICON_CY, col);
|
||||
} else if (typeIdx == 717 || typeIdx == 731) {
|
||||
blitIcon(dc, mIconSunset, cx, ICON_CY, col);
|
||||
} else if (typeIdx == 306 || typeIdx == 639 || typeIdx == 300
|
||||
|| typeIdx == 301 || typeIdx == 302 || typeIdx == 304
|
||||
|| typeIdx == 305 || typeIdx == 308 || typeIdx == 309
|
||||
|| typeIdx == 310 || typeIdx == 307) {
|
||||
blitIcon(dc, mIconWeather, cx, ICON_CY, col);
|
||||
} else if (typeIdx == 250 || typeIdx == 251 || typeIdx == 257) {
|
||||
iconBattery(dc, cx - 14.4 * scale, cy, scale, col, level);
|
||||
} else {
|
||||
var r = 13.0 * scale;
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.setPenWidth(maxNum(2, px(3.0 * scale)));
|
||||
if (typeIdx == 1) { // calories: flame
|
||||
if (typeIdx == 2 || typeIdx == 603) { // calories: flame
|
||||
dc.fillPolygon([[px(cx), px(cy - r)],
|
||||
[px(cx + r * 0.72), px(cy + r * 0.25)],
|
||||
[px(cx + r * 0.40), px(cy + r * 0.85)],
|
||||
[px(cx - r * 0.40), px(cy + r * 0.85)],
|
||||
[px(cx - r * 0.72), px(cy + r * 0.25)]]);
|
||||
} else if (typeIdx == 2) { // steps: two prints
|
||||
} else if (typeIdx == 1 || typeIdx == 14 || typeIdx == 707) { // steps
|
||||
dc.fillRoundedRectangle(px(cx - r * 0.85), px(cy - r * 0.75),
|
||||
px(r * 0.62), px(r * 1.15), px(r * 0.3));
|
||||
dc.fillRoundedRectangle(px(cx + r * 0.22), px(cy - r * 0.35),
|
||||
px(r * 0.62), px(r * 1.15), px(r * 0.3));
|
||||
} else if (typeIdx == 3) { // distance: arrow
|
||||
} else if (typeIdx == 3 || typeIdx == 9 || typeIdx == 615) { // distance
|
||||
dc.fillPolygon([[px(cx), px(cy - r)],
|
||||
[px(cx + r * 0.75), px(cy + r * 0.8)],
|
||||
[px(cx), px(cy + r * 0.35)],
|
||||
[px(cx - r * 0.75), px(cy + r * 0.8)]]);
|
||||
} else if (typeIdx == 9) { // heart rate
|
||||
} else if (typeIdx == 618 || typeIdx == 276 || typeIdx == 623) { // heart
|
||||
dc.fillCircle(px(cx - r * 0.38), px(cy - r * 0.28), px(r * 0.44));
|
||||
dc.fillCircle(px(cx + r * 0.38), px(cy - r * 0.28), px(r * 0.44));
|
||||
dc.fillPolygon([[px(cx - r * 0.78), px(cy - r * 0.12)],
|
||||
[px(cx + r * 0.78), px(cy - r * 0.12)],
|
||||
[px(cx), px(cy + r * 0.85)]]);
|
||||
} else { // floors: stairs
|
||||
} else if (typeIdx == 4 || typeIdx == 5 || typeIdx == 6) { // stairs
|
||||
for (var i = 0; i < 3; i++) {
|
||||
dc.fillRectangle(px(cx - r * 0.85 + r * 0.58 * i),
|
||||
px(cy + r * 0.6 - r * 0.55 * i),
|
||||
px(r * 0.62), px(r * 0.4));
|
||||
}
|
||||
}
|
||||
// Everything else -- dates, week numbers, custom text -- reads fine
|
||||
// without a glyph, so no icon is drawn.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,14 +435,10 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
}
|
||||
|
||||
function drawTopComplication(dc as Dc, ti as Number, typeIdx as Number, isLeft as Boolean,
|
||||
info as ActivityMonitor.Info, weather as Weather.CurrentConditions?,
|
||||
stats as System.Stats, ds as System.DeviceSettings) as Void {
|
||||
ctx as Fields.Ctx, level as Float) as Void {
|
||||
var x = isLeft ? LEFT_X : RIGHT_X;
|
||||
var level = 0.0f;
|
||||
if (stats != null && stats.battery != null) { level = stats.battery / 100.0; }
|
||||
drawComplicationIcon(dc, x, ICON_CY, 1.0, Themes.ACCENT[ti] as Number, typeIdx, level);
|
||||
text(dc, x, VALUE_CY, mFontComp,
|
||||
complicationValue(typeIdx, info, weather, stats, ds),
|
||||
text(dc, x, VALUE_CY, mFontComp, Fields.value(typeIdx, ctx),
|
||||
Graphics.TEXT_JUSTIFY_CENTER, Themes.TEXTPRIMARY[ti] as Number);
|
||||
}
|
||||
|
||||
@@ -615,15 +468,12 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
// FORMAT_SHORT, not FORMAT_MEDIUM: only the short form returns
|
||||
// day_of_week and month as numbers rather than localised strings.
|
||||
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
text(dc, DOW_CX, BAND_CY, mFontBand, mDow[g.day_of_week - 1],
|
||||
text(dc, DOW_CX, BAND_CY, mFontBand, Fields.dow(g),
|
||||
Graphics.TEXT_JUSTIFY_CENTER, col);
|
||||
// DateFormat is "$1$ $2$" in English and "$1$$2$日" in Chinese, where
|
||||
// the Chinese month names already carry their own 月.
|
||||
text(dc, MD_CX, BAND_CY, mFontBand,
|
||||
Lang.format(mDateFmt, [mMon[g.month - 1], g.day.format("%d")]),
|
||||
text(dc, MD_CX, BAND_CY, mFontBand, Fields.monthDay(g),
|
||||
Graphics.TEXT_JUSTIFY_CENTER, col);
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
text(dc, AMPM_R, BAND_CY, mFontBand, mMeridiem[g.hour >= 12 ? 1 : 0],
|
||||
text(dc, AMPM_R, BAND_CY, mFontBand, Fields.meridiem(g),
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, col);
|
||||
}
|
||||
}
|
||||
@@ -653,49 +503,120 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
dc.fillRectangle(px(COLON_X), px(COLON_Y2), px(COLON_W), px(COLON_H));
|
||||
}
|
||||
|
||||
function drawBottom(dc as Dc, ti as Number, info as ActivityMonitor.Info,
|
||||
weather as Weather.CurrentConditions?, stats as System.Stats,
|
||||
ds as System.DeviceSettings) as Void {
|
||||
function drawBottom(dc as Dc, ti as Number, ctx as Fields.Ctx) as Void {
|
||||
var value = Themes.TEXTPRIMARY[ti] as Number;
|
||||
var label = Themes.ACCENT[ti] as Number;
|
||||
var left = getSetting("BottomLeft", 3);
|
||||
var right = getSetting("BottomRight", 2);
|
||||
text(dc, DIST_R, BOT_VAL_CY, mFontComp,
|
||||
complicationValue(left, info, weather, stats, ds),
|
||||
var right = getSetting("BottomRight", 1);
|
||||
text(dc, DIST_R, BOT_VAL_CY, mFontComp, Fields.value(left, ctx),
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, value);
|
||||
text(dc, DIST_R, BOT_LAB_CY, mFontComp, fieldLabel(left),
|
||||
text(dc, DIST_R, BOT_LAB_CY, mFontComp, Fields.label(left),
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, label);
|
||||
text(dc, STEP_L, BOT_VAL_CY, mFontComp,
|
||||
complicationValue(right, info, weather, stats, ds),
|
||||
text(dc, STEP_L, BOT_VAL_CY, mFontComp, Fields.value(right, ctx),
|
||||
Graphics.TEXT_JUSTIFY_LEFT, value);
|
||||
text(dc, STEP_L, BOT_LAB_CY, mFontComp, fieldLabel(right),
|
||||
text(dc, STEP_L, BOT_LAB_CY, mFontComp, Fields.label(right),
|
||||
Graphics.TEXT_JUSTIFY_LEFT, label);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ AOD
|
||||
function dim(col as Number, f as Float) as Number {
|
||||
var r = (((col >> 16) & 0xFF) * f).toNumber();
|
||||
var g = (((col >> 8) & 0xFF) * f).toNumber();
|
||||
var b = ((col & 0xFF) * f).toNumber();
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
// Low-power face: no band fill, no unlit ticks, no complications -- just a
|
||||
// dimmed time, the lit part of the ring, and the date. The whole thing
|
||||
// walks a few pixels each minute so no pixel stays lit in one place.
|
||||
function drawAmbient(dc as Dc, ti as Number, ds as System.DeviceSettings,
|
||||
pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void {
|
||||
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
var ox = ((g.min % 5) - 2) * 2.0 / mS; // +/- 4 device px, in design units
|
||||
var oy = ((g.min / 5) % 5 - 2) * 2.0 / mS;
|
||||
|
||||
var top = Themes.RINGTOP[ti] as Array<Number>;
|
||||
var bot = Themes.RINGBOTTOM[ti] as Array<Number>;
|
||||
var litTR = (pTR * TOP_N).toNumber();
|
||||
var litTL = (pTL * TOP_N).toNumber();
|
||||
var litBR = (pBR * BOT_N).toNumber();
|
||||
var litBL = (pBL * BOT_N).toNumber();
|
||||
for (var i = 0; i < TOP_N; i++) {
|
||||
var a = TOP_PH + TOP_PITCH * i;
|
||||
if (i >= TOP_N - litTR) {
|
||||
dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
|
||||
tick(dc, a, TOP_HW);
|
||||
}
|
||||
if (i >= TOP_N - litTL) {
|
||||
dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
|
||||
tick(dc, -a, TOP_HW);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < BOT_N; i++) {
|
||||
var a = BOT_PH + BOT_PITCH * i;
|
||||
if (i < litBL) {
|
||||
dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
|
||||
tick(dc, 180.0 + a, BOT_HW);
|
||||
}
|
||||
if (i < litBR) {
|
||||
dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
|
||||
tick(dc, 180.0 - a, BOT_HW);
|
||||
}
|
||||
}
|
||||
|
||||
var h = g.hour;
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
var hours = Themes.HOURS[ti] as Array<Number>;
|
||||
var mins = Themes.MINUTES[ti] as Array<Number>;
|
||||
text(dc, HOURS_R + ox, TIME_CY + oy, mFontTime, h.format("%d"),
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, dim(hours[hours.size() / 2], 0.55));
|
||||
text(dc, MINUTES_L + ox, TIME_CY + oy, mFontTime, g.min.format("%02d"),
|
||||
Graphics.TEXT_JUSTIFY_LEFT, dim(mins[mins.size() / 2], 0.55));
|
||||
dc.setColor(dim(Themes.COLON[ti] as Number, 0.55), Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillRectangle(px(COLON_X + ox), px(COLON_Y1 + oy), px(COLON_W), px(COLON_H));
|
||||
dc.fillRectangle(px(COLON_X + ox), px(COLON_Y2 + oy), px(COLON_W), px(COLON_H));
|
||||
|
||||
var band = dim(Themes.ACCENT[ti] as Number, 0.55);
|
||||
text(dc, DOW_CX + ox, BAND_CY + oy, mFontBand, Fields.dow(g),
|
||||
Graphics.TEXT_JUSTIFY_CENTER, band);
|
||||
text(dc, MD_CX + ox, BAND_CY + oy, mFontBand, Fields.monthDay(g),
|
||||
Graphics.TEXT_JUSTIFY_CENTER, band);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- main
|
||||
function onUpdate(dc as Dc) as Void {
|
||||
if (mW != dc.getWidth() || mDow == null) { onLayout(dc); }
|
||||
if (mW != dc.getWidth() || !Fields.ready()) { onLayout(dc); }
|
||||
if (dc has :setAntiAlias) { dc.setAntiAlias(true); }
|
||||
|
||||
var ti = themeIndex();
|
||||
var info = ActivityMonitor.getInfo();
|
||||
var stats = System.getSystemStats();
|
||||
var ds = System.getDeviceSettings();
|
||||
var weather = Weather.getCurrentConditions();
|
||||
var ctx = new Fields.Ctx(ds, getSetting("TempUnit", 0));
|
||||
var info = ctx.info();
|
||||
var stats = ctx.stats();
|
||||
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
|
||||
dc.clear();
|
||||
|
||||
drawRing(dc, ti,
|
||||
metricProgress(getSetting("RingTR", 0), info),
|
||||
metricProgress(getSetting("RingTL", 1), info),
|
||||
metricProgress(getSetting("RingBR", 4), info),
|
||||
metricProgress(getSetting("RingBL", 3), info));
|
||||
var pTR = metricProgress(getSetting("RingTR", 0), info);
|
||||
var pTL = metricProgress(getSetting("RingTL", 1), info);
|
||||
var pBR = metricProgress(getSetting("RingBR", 4), info);
|
||||
var pBL = metricProgress(getSetting("RingBL", 3), info);
|
||||
|
||||
if (mSleeping && mBurnIn) {
|
||||
drawAmbient(dc, ti, ds, pTR, pTL, pBR, pBL);
|
||||
return;
|
||||
}
|
||||
|
||||
drawRing(dc, ti, pTR, pTL, pBR, pBL);
|
||||
|
||||
if (getBool("ShowTop", true)) {
|
||||
var level = 0.0f;
|
||||
if (stats != null && stats.battery != null) { level = stats.battery / 100.0; }
|
||||
drawBattery(dc, ti, stats);
|
||||
drawTopComplication(dc, ti, getSetting("LeftTop", 6), true, info, weather, stats, ds);
|
||||
drawTopComplication(dc, ti, getSetting("RightTop", 8), false, info, weather, stats, ds);
|
||||
drawTopComplication(dc, ti, getSetting("LeftTop", 712), true, ctx, level);
|
||||
drawTopComplication(dc, ti, getSetting("RightTop", 639), false, ctx, level);
|
||||
}
|
||||
|
||||
drawTime(dc, ti, ds);
|
||||
@@ -705,7 +626,7 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
}
|
||||
|
||||
if (getBool("ShowBottom", true)) {
|
||||
drawBottom(dc, ti, info, weather, stats, ds);
|
||||
drawBottom(dc, ti, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
source/FieldTable.mc
Normal file
22
source/FieldTable.mc
Normal file
@@ -0,0 +1,22 @@
|
||||
// AUTO-GENERATED by tools/gen_fields.py -- do not edit by hand.
|
||||
// Ids follow https://watchface.io/docs/datafields; only the fields a
|
||||
// Connect IQ watch face can source on-device are listed.
|
||||
import Toybox.Lang;
|
||||
|
||||
module FieldTable {
|
||||
|
||||
// Field ids, in the same order as the Labels resource string.
|
||||
const IDS = [
|
||||
0,860,880,864,865,851,872,852,855,870,856,853,868,858,859,873,1,14,707,2,603,150,
|
||||
152,154,151,3,4,5,6,9,10,618,276,12,635,11,623,8,15,281,742,13,278,277,250,251,257,
|
||||
253,255,254,256,252,615,616,733,712,717,700,731,706,732,306,639,300,301,302,305,304,
|
||||
308,307,310,309,703,704,705
|
||||
];
|
||||
|
||||
function indexOf(id as Number) as Number {
|
||||
for (var i = 0; i < IDS.size(); i++) {
|
||||
if (IDS[i] == id) { return i; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
491
source/Fields.mc
Normal file
491
source/Fields.mc
Normal file
@@ -0,0 +1,491 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Math;
|
||||
import Toybox.System;
|
||||
import Toybox.Time;
|
||||
import Toybox.Time.Gregorian;
|
||||
import Toybox.Application;
|
||||
import Toybox.ActivityMonitor;
|
||||
import Toybox.Activity;
|
||||
import Toybox.UserProfile;
|
||||
import Toybox.SensorHistory;
|
||||
import Toybox.Weather;
|
||||
import Toybox.Position;
|
||||
import Toybox.WatchUi;
|
||||
import FieldTable;
|
||||
|
||||
// Everything a complication slot can show. Ids match
|
||||
// https://watchface.io/docs/datafields so the two catalogues line up; see
|
||||
// tools/gen_fields.py for the table that drives the settings and the labels.
|
||||
//
|
||||
// Only fields Connect IQ can answer from the watch itself are here. The
|
||||
// week-to-date and 7/28-day per-sport aggregates in that catalogue come from a
|
||||
// phone-side Garmin Connect integration, and the OpenWeatherMap / StormGlass /
|
||||
// third-party-app fields need network calls and API keys; neither is something
|
||||
// a self-contained watch face can produce.
|
||||
module Fields {
|
||||
|
||||
const DASH = "--";
|
||||
|
||||
var mDow as Array<String>?;
|
||||
var mMon as Array<String>?;
|
||||
var mLabels as Array<String>?;
|
||||
var mMeridiem as Array<String>?;
|
||||
var mDateFmt as String = "$1$ $2$";
|
||||
|
||||
function loadStrings() as Void {
|
||||
mDow = splitCsv(WatchUi.loadResource(Rez.Strings.DowNames) as String);
|
||||
mMon = splitCsv(WatchUi.loadResource(Rez.Strings.MonNames) as String);
|
||||
mLabels = splitCsv(WatchUi.loadResource(Rez.Strings.FieldLabels) as String);
|
||||
mMeridiem = splitCsv(WatchUi.loadResource(Rez.Strings.Meridiem) as String);
|
||||
mDateFmt = WatchUi.loadResource(Rez.Strings.DateFormat) as String;
|
||||
}
|
||||
|
||||
function ready() as Boolean { return mDow != null; }
|
||||
|
||||
// Monkey C has no String.split.
|
||||
function splitCsv(str as String) as Array<String> {
|
||||
var out = [] as Array<String>;
|
||||
var rest = str;
|
||||
var cut = rest.find(",");
|
||||
while (cut != null) {
|
||||
out.add(rest.substring(0, cut) as String);
|
||||
rest = rest.substring(cut + 1, rest.length()) as String;
|
||||
cut = rest.find(",");
|
||||
}
|
||||
out.add(rest);
|
||||
return out;
|
||||
}
|
||||
|
||||
function dow(g as Gregorian.Info) as String {
|
||||
return mDow == null ? "" : mDow[g.day_of_week - 1];
|
||||
}
|
||||
|
||||
function month(g as Gregorian.Info) as String {
|
||||
return mMon == null ? "" : mMon[g.month - 1];
|
||||
}
|
||||
|
||||
function monthDay(g as Gregorian.Info) as String {
|
||||
return Lang.format(mDateFmt, [month(g), g.day.format("%d")]);
|
||||
}
|
||||
|
||||
function meridiem(g as Gregorian.Info) as String {
|
||||
return mMeridiem == null ? "" : mMeridiem[g.hour >= 12 ? 1 : 0];
|
||||
}
|
||||
|
||||
// Short caption printed under a bottom-row value.
|
||||
function label(id as Number) as String {
|
||||
if (mLabels == null) { return ""; }
|
||||
var i = FieldTable.indexOf(id);
|
||||
return i < mLabels.size() ? mLabels[i] : "";
|
||||
}
|
||||
|
||||
// One of these is built per onUpdate and handed to every slot, so the
|
||||
// expensive lookups (forecast, sensor history, user profile) happen at most
|
||||
// once per draw and only if some slot actually asks for them.
|
||||
class Ctx {
|
||||
var greg as Gregorian.Info;
|
||||
var is24 as Boolean = true;
|
||||
var metric as Boolean = true;
|
||||
var fahrenheit as Boolean = false;
|
||||
|
||||
private var mInfo as ActivityMonitor.Info?;
|
||||
private var mStats as System.Stats?;
|
||||
private var mDs as System.DeviceSettings?;
|
||||
private var mAct as Activity.Info?;
|
||||
private var mWx as Weather.CurrentConditions?;
|
||||
private var mFc as Array<Weather.DailyForecast>?;
|
||||
private var mProf as UserProfile.Profile?;
|
||||
private var mGotAct = false;
|
||||
private var mGotWx = false;
|
||||
private var mGotFc = false;
|
||||
private var mGotProf = false;
|
||||
|
||||
function initialize(ds as System.DeviceSettings?, tempUnit as Number) {
|
||||
mDs = ds;
|
||||
greg = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
if (ds != null) {
|
||||
is24 = ds.is24Hour;
|
||||
metric = (ds.distanceUnits != System.UNIT_STATUTE);
|
||||
fahrenheit = (tempUnit == 2)
|
||||
|| (tempUnit == 0 && ds.temperatureUnits == System.UNIT_STATUTE);
|
||||
}
|
||||
if (tempUnit == 1) { fahrenheit = false; }
|
||||
}
|
||||
|
||||
function ds() as System.DeviceSettings? { return mDs; }
|
||||
|
||||
function info() as ActivityMonitor.Info? {
|
||||
if (mInfo == null) { mInfo = ActivityMonitor.getInfo(); }
|
||||
return mInfo;
|
||||
}
|
||||
|
||||
function stats() as System.Stats? {
|
||||
if (mStats == null) { mStats = System.getSystemStats(); }
|
||||
return mStats;
|
||||
}
|
||||
|
||||
function act() as Activity.Info? {
|
||||
if (!mGotAct) { mAct = Activity.getActivityInfo(); mGotAct = true; }
|
||||
return mAct;
|
||||
}
|
||||
|
||||
function wx() as Weather.CurrentConditions? {
|
||||
if (!mGotWx) { mWx = Weather.getCurrentConditions(); mGotWx = true; }
|
||||
return mWx;
|
||||
}
|
||||
|
||||
function forecast() as Weather.DailyForecast? {
|
||||
if (!mGotFc) { mFc = Weather.getDailyForecast(); mGotFc = true; }
|
||||
if (mFc != null && mFc.size() > 0) { return mFc[0]; }
|
||||
return null;
|
||||
}
|
||||
|
||||
function profile() as UserProfile.Profile? {
|
||||
if (!mGotProf) { mProf = UserProfile.getProfile(); mGotProf = true; }
|
||||
return mProf;
|
||||
}
|
||||
|
||||
function where() as Position.Location? {
|
||||
var w = wx();
|
||||
if (w != null && w.observationLocationPosition != null) {
|
||||
return w.observationLocationPosition;
|
||||
}
|
||||
var a = act();
|
||||
if (a != null && a.currentLocation != null) { return a.currentLocation; }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- helpers
|
||||
function num(v as Numeric?) as String {
|
||||
return v == null ? DASH : v.format("%d");
|
||||
}
|
||||
|
||||
function one(v as Float?) as String {
|
||||
return v == null ? DASH : v.format("%.1f");
|
||||
}
|
||||
|
||||
function clock(m as Time.Moment?, c as Ctx) as String {
|
||||
if (m == null) { return DASH; }
|
||||
return clockInfo(Gregorian.info(m, Time.FORMAT_SHORT), c);
|
||||
}
|
||||
|
||||
function clockInfo(g as Gregorian.Info, c as Ctx) as String {
|
||||
var h = g.hour;
|
||||
if (!c.is24) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
return h.format("%d") + ":" + g.min.format("%02d");
|
||||
}
|
||||
|
||||
function offsetClock(minutes as Number, c as Ctx) as String {
|
||||
var m = Time.now().add(new Time.Duration(minutes * 60));
|
||||
return clock(m, c);
|
||||
}
|
||||
|
||||
function temp(celsius as Numeric?, c as Ctx) as String {
|
||||
if (celsius == null) { return DASH; }
|
||||
var t = celsius.toFloat();
|
||||
if (c.fahrenheit) { t = t * 9.0 / 5.0 + 32.0; }
|
||||
return t.format("%d") + "°";
|
||||
}
|
||||
|
||||
// ActivityMonitor distances are centimetres.
|
||||
function dist(cm as Numeric?, c as Ctx) as String {
|
||||
if (cm == null) { return DASH; }
|
||||
var km = cm.toFloat() / 100000.0;
|
||||
return (c.metric ? km : km * 0.621371).format("%.1f");
|
||||
}
|
||||
|
||||
function metres(m as Numeric?, c as Ctx) as String {
|
||||
if (m == null) { return DASH; }
|
||||
var v = m.toFloat();
|
||||
return (c.metric ? v : v * 3.28084).format("%d");
|
||||
}
|
||||
|
||||
function rounded(v as Numeric?) as String {
|
||||
if (v == null) { return DASH; }
|
||||
var n = v.toNumber();
|
||||
if (n < 1000) { return n.format("%d"); }
|
||||
return (n / 1000.0).format("%.1f") + "k";
|
||||
}
|
||||
|
||||
function setting(name as String, def as Number) as Number {
|
||||
var v = Application.Properties.getValue(name);
|
||||
if (v == null) { return def; }
|
||||
return v as Number;
|
||||
}
|
||||
|
||||
function settingText(name as String) as String {
|
||||
var v = Application.Properties.getValue(name);
|
||||
if (v == null) { return ""; }
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
// Newest reading out of a SensorHistory iterator, or null.
|
||||
function latest(iter) as Numeric? {
|
||||
if (iter == null) { return null; }
|
||||
var s = iter.next();
|
||||
if (s == null) { return null; }
|
||||
return s.data;
|
||||
}
|
||||
|
||||
function dayOfYear(g as Gregorian.Info) as Number {
|
||||
var cum = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
var d = cum[g.month - 1] + g.day;
|
||||
var y = g.year;
|
||||
var leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
||||
if (leap && g.month > 2) { d = d + 1; }
|
||||
return d;
|
||||
}
|
||||
|
||||
// ISO 8601 week: weeks start Monday, week 1 holds the first Thursday.
|
||||
function isoWeek(g as Gregorian.Info) as Number {
|
||||
var dow = g.day_of_week - 1; // 0 = Sunday
|
||||
var iso = dow == 0 ? 7 : dow; // 1 = Monday .. 7 = Sunday
|
||||
var w = (dayOfYear(g) - iso + 10) / 7;
|
||||
if (w < 1) { return 52; }
|
||||
if (w > 52) { return 1; }
|
||||
return w;
|
||||
}
|
||||
|
||||
// Days since the new moon of 2000-01-06, folded into one synodic month.
|
||||
function moonAge(g as Gregorian.Info) as Float {
|
||||
var y = g.year;
|
||||
var m = g.month;
|
||||
if (m <= 2) { y = y - 1; m = m + 12; }
|
||||
var a = y / 100;
|
||||
var b = 2 - a + a / 4;
|
||||
var jd = (365.25 * (y + 4716)).toNumber() + (30.6001 * (m + 1)).toNumber()
|
||||
+ g.day + b - 1524.5 + g.hour / 24.0;
|
||||
// Monkey C has no float modulo.
|
||||
var syn = 29.530588853;
|
||||
var x = jd - 2451550.1;
|
||||
var age = x - (x / syn).toNumber() * syn;
|
||||
if (age < 0) { age = age + syn; }
|
||||
return age.toFloat();
|
||||
}
|
||||
|
||||
function windDir(bearingDeg as Numeric?) as String {
|
||||
if (bearingDeg == null) { return DASH; }
|
||||
var pts = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
|
||||
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
|
||||
var b = bearingDeg.toFloat();
|
||||
while (b < 0) { b = b + 360.0; }
|
||||
var i = ((b + 11.25) / 22.5).toNumber() % 16;
|
||||
return pts[i];
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- value
|
||||
function value(id as Number, c as Ctx) as String {
|
||||
// ---- date / time
|
||||
if (id == 860) { return clockInfo(c.greg, c); }
|
||||
if (id == 880) {
|
||||
var utc = Gregorian.utcInfo(Time.now(), Time.FORMAT_SHORT);
|
||||
return clockInfo(utc, c);
|
||||
}
|
||||
if (id == 864) { return offsetClock(setting("AltOffset1", 0), c); }
|
||||
if (id == 865) { return offsetClock(setting("AltOffset2", 0), c); }
|
||||
if (id == 855) { return c.greg.day.format("%d"); }
|
||||
if (id == 868) { return c.greg.month.format("%d"); }
|
||||
if (id == 859) { return isoWeek(c.greg).format("%d"); }
|
||||
if (id == 873) { return ((dayOfYear(c.greg) + 6) / 7).format("%d"); }
|
||||
if (id == 870) { return dow(c.greg); }
|
||||
if (id == 856) { return dow(c.greg); }
|
||||
if (id == 853) { return month(c.greg); }
|
||||
if (id == 852) { return monthDay(c.greg); }
|
||||
if (id == 872) { return dow(c.greg) + " " + c.greg.day.format("%d"); }
|
||||
if (id == 851) { return dow(c.greg) + " " + monthDay(c.greg); }
|
||||
if (id == 858) { return meridiem(c.greg); }
|
||||
|
||||
// ---- activity
|
||||
var i = c.info();
|
||||
if (id == 1) { return i == null ? DASH : num(i.steps); }
|
||||
if (id == 14) { return i == null ? DASH : rounded(i.steps); }
|
||||
if (id == 707) {
|
||||
if (i == null || i.steps == null || i.stepGoal == null) { return DASH; }
|
||||
var left = i.stepGoal - i.steps;
|
||||
return left > 0 ? left.format("%d") : "0";
|
||||
}
|
||||
if (id == 2) { return i == null ? DASH : num(i.calories); }
|
||||
if (id == 603) {
|
||||
if (i == null || i.calories == null) { return DASH; }
|
||||
var p = c.profile();
|
||||
if (p == null || p.weight == null || p.height == null) { return num(i.calories); }
|
||||
return num(i.calories); // device reports total only
|
||||
}
|
||||
if (id == 150 || id == 152 || id == 154) {
|
||||
if (i == null || i.activeMinutesDay == null) { return DASH; }
|
||||
var am = i.activeMinutesDay;
|
||||
if (id == 152) { return num(am.moderate); }
|
||||
if (id == 154) { return num(am.vigorous); }
|
||||
return num(am.total);
|
||||
}
|
||||
if (id == 151) {
|
||||
if (i == null || i.activeMinutesWeek == null) { return DASH; }
|
||||
return num(i.activeMinutesWeek.total);
|
||||
}
|
||||
if (id == 3) { return i == null ? DASH : dist(i.distance, c); }
|
||||
if (id == 4) { return i == null ? DASH : num(i.floorsClimbed); }
|
||||
if (id == 5) { return i == null ? DASH : num(i.floorsDescended); }
|
||||
if (id == 6) { return i == null ? DASH : metres(i.metersClimbed, c); }
|
||||
if (id == 9) { return i == null ? DASH : dist(i.pushDistance, c); }
|
||||
if (id == 10) { return i == null ? DASH : num(i.pushes); }
|
||||
if (id == 8 || id == 15) {
|
||||
if (i == null || i.moveBarLevel == null) { return DASH; }
|
||||
var lvl = i.moveBarLevel;
|
||||
if (id == 15) { lvl = ActivityMonitor.MOVE_BAR_LEVEL_MAX - lvl; }
|
||||
return lvl.format("%d");
|
||||
}
|
||||
if (id == 13) { return i == null ? DASH : num(i.timeToRecovery); }
|
||||
if (id == 11) { return i == null ? DASH : num(i.respirationRate); }
|
||||
if (id == 12) { return i == null ? DASH : num(i.stressScore); }
|
||||
|
||||
// ---- body
|
||||
if (id == 618) {
|
||||
var a = c.act();
|
||||
if (a != null && a.currentHeartRate != null) { return num(a.currentHeartRate); }
|
||||
var h = ActivityMonitor.getHeartRateHistory(1, true);
|
||||
if (h != null) {
|
||||
var s = h.next();
|
||||
if (s != null && s.heartRate != null
|
||||
&& s.heartRate != ActivityMonitor.INVALID_HR_SAMPLE) {
|
||||
return num(s.heartRate);
|
||||
}
|
||||
}
|
||||
return DASH;
|
||||
}
|
||||
if (id == 276) {
|
||||
var p = c.profile();
|
||||
return (p == null) ? DASH : num(p.restingHeartRate);
|
||||
}
|
||||
if (id == 635) {
|
||||
if (!(SensorHistory has :getOxygenSaturationHistory)) { return DASH; }
|
||||
return num(latest(SensorHistory.getOxygenSaturationHistory({:period => 1})));
|
||||
}
|
||||
if (id == 623) {
|
||||
if (!(SensorHistory has :getBodyBatteryHistory)) { return DASH; }
|
||||
return num(latest(SensorHistory.getBodyBatteryHistory({:period => 1})));
|
||||
}
|
||||
if (id == 281 || id == 742) {
|
||||
var p = c.profile();
|
||||
if (p == null || p.weight == null) { return DASH; }
|
||||
var kg = p.weight.toFloat() / 1000.0;
|
||||
if (id == 281) {
|
||||
return (c.metric ? kg : kg * 2.20462).format("%.1f");
|
||||
}
|
||||
if (p.height == null || p.height <= 0) { return DASH; }
|
||||
var m = p.height.toFloat() / 100.0;
|
||||
return (kg / (m * m)).format("%.1f");
|
||||
}
|
||||
if (id == 277 || id == 278) {
|
||||
var p = c.profile();
|
||||
if (p == null) { return DASH; }
|
||||
return num(id == 278 ? p.vo2maxRunning : p.vo2maxCycling);
|
||||
}
|
||||
|
||||
// ---- system
|
||||
var st = c.stats();
|
||||
if (id == 250) {
|
||||
return st == null || st.battery == null ? DASH
|
||||
: st.battery.format("%d") + "%";
|
||||
}
|
||||
if (id == 251) {
|
||||
if (st == null || !(st has :batteryInDays) || st.batteryInDays == null) {
|
||||
return DASH;
|
||||
}
|
||||
return st.batteryInDays.format("%d") + "d";
|
||||
}
|
||||
if (id == 257) {
|
||||
var pct = (st == null || st.battery == null) ? DASH : st.battery.format("%d");
|
||||
var d = (st == null || !(st has :batteryInDays) || st.batteryInDays == null)
|
||||
? DASH : st.batteryInDays.format("%d");
|
||||
return pct + "/" + d;
|
||||
}
|
||||
if (id == 252) {
|
||||
if (st == null || !(st has :solarIntensity) || st.solarIntensity == null) {
|
||||
return DASH;
|
||||
}
|
||||
return st.solarIntensity.format("%d") + "%";
|
||||
}
|
||||
var ds = c.ds();
|
||||
if (id == 253) { return ds == null ? DASH : num(ds.alarmCount); }
|
||||
if (id == 255) { return ds == null ? DASH : num(ds.notificationCount); }
|
||||
if (id == 254) {
|
||||
return ds == null ? DASH : (ds.doNotDisturb ? "ON" : "OFF");
|
||||
}
|
||||
if (id == 256) {
|
||||
return ds == null ? DASH : (ds.phoneConnected ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
// ---- environment
|
||||
if (id == 615) {
|
||||
var a = c.act();
|
||||
return (a == null) ? DASH : metres(a.altitude, c);
|
||||
}
|
||||
if (id == 616 || id == 733) {
|
||||
var a = c.act();
|
||||
if (a == null) { return DASH; }
|
||||
var pa = (id == 616) ? a.meanSeaLevelPressure : a.ambientPressure;
|
||||
if (pa == null) { return DASH; }
|
||||
return (pa.toFloat() / 100.0).format("%d");
|
||||
}
|
||||
if (id == 712 || id == 717 || id == 700 || id == 731) {
|
||||
var loc = c.where();
|
||||
if (loc == null) { return DASH; }
|
||||
var now = Time.now();
|
||||
var rise = Weather.getSunrise(loc, now);
|
||||
var set = Weather.getSunset(loc, now);
|
||||
if (id == 712) { return clock(rise, c); }
|
||||
if (id == 717) { return clock(set, c); }
|
||||
if (rise == null || set == null) { return DASH; }
|
||||
var next = (now.compare(rise) < 0) ? rise
|
||||
: ((now.compare(set) < 0) ? set : rise);
|
||||
if (id == 700) { return clock(next, c); }
|
||||
var secs = next.compare(now);
|
||||
if (secs < 0) { secs = secs + 86400; }
|
||||
return (secs / 3600).format("%d") + "h" + ((secs % 3600) / 60).format("%02d");
|
||||
}
|
||||
if (id == 706) { return moonAge(c.greg).format("%.1f"); }
|
||||
if (id == 732) {
|
||||
var age = moonAge(c.greg);
|
||||
var lit = (1.0 - Math.cos(2.0 * Math.PI * age / 29.530588853)) / 2.0;
|
||||
return (lit * 100.0).format("%d") + "%";
|
||||
}
|
||||
|
||||
// ---- weather
|
||||
var w = c.wx();
|
||||
if (id == 306) { return w == null ? DASH : temp(w.temperature, c); }
|
||||
if (id == 300) { return w == null ? DASH : temp(w.feelsLikeTemperature, c); }
|
||||
if (id == 305) { return w == null ? DASH : num(w.relativeHumidity) + "%"; }
|
||||
if (id == 304) {
|
||||
if (w == null || !(w has :precipitationChance)) { return DASH; }
|
||||
return num(w.precipitationChance) + "%";
|
||||
}
|
||||
if (id == 308) {
|
||||
if (w == null || w.windSpeed == null) { return DASH; }
|
||||
var kmh = w.windSpeed.toFloat() * 3.6;
|
||||
return (c.metric ? kmh : kmh * 0.621371).format("%d");
|
||||
}
|
||||
if (id == 307) { return w == null ? DASH : num(w.windBearing); }
|
||||
if (id == 310) { return w == null ? DASH : windDir(w.windBearing); }
|
||||
if (id == 309) {
|
||||
if (w == null || w.observationTime == null) { return DASH; }
|
||||
return clock(w.observationTime, c);
|
||||
}
|
||||
if (id == 301 || id == 302 || id == 639) {
|
||||
var f = c.forecast();
|
||||
if (f == null) { return DASH; }
|
||||
if (id == 301) { return temp(f.highTemperature, c); }
|
||||
if (id == 302) { return temp(f.lowTemperature, c); }
|
||||
return temp(f.highTemperature, c) + "/" + temp(f.lowTemperature, c);
|
||||
}
|
||||
|
||||
// ---- custom
|
||||
if (id == 703) { return settingText("Custom1"); }
|
||||
if (id == 704) { return settingText("Custom2"); }
|
||||
if (id == 705) { return settingText("Custom3"); }
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user