Architecture
Fenix8V3View had grown to 632 lines mixing layout constants, font
metrics, icon drawing, data lookup and orchestration. Split into:
Layout the 500-canvas constants, scaling, font choice and text drawing
Icons the three bitmap icons plus the primitive glyphs
Settings every stored preference and its default, in one place
Fields unchanged in scope, but value() is now seven category
functions that each return null for ids they do not own --
the ids interleave, so dispatching on ranges does not work
View what is left: the drawing order
The view is down to 359 lines and no longer reaches for a property or a
font metric directly.
Verification
Added a harness that walks all 75 ids in the simulator and reports each
rendered value with its pixel width. It caught a real one: pushDistance
and pushes compile on every device but throw "Symbol Not Found" at
runtime on a fenix 8 -- they are wheelchair counters that only some
firmware carries. Every optional ActivityMonitor, UserProfile, Weather
and Activity member is now behind a `has` check.
The same run showed two date fields overflowing their slot, so long
values now step down a font size rather than running into the ring.
Always-on mode checked against the simulator's Always-On display mode:
band fill gone, unlit ticks gone, slots hidden, time and date dimmed.
Also: Chinese "Day Date" was missing its 日, since it built the string by
hand instead of going through the localised date format.
All 17 devices build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
586 lines
22 KiB
MonkeyC
586 lines
22 KiB
MonkeyC
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;
|
|
import Settings;
|
|
|
|
// 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")]);
|
|
}
|
|
|
|
// "10" in English, "10日" in Chinese -- the format string carries the
|
|
// suffix, so feed it an empty month.
|
|
function dayOnly(g as Gregorian.Info) as String {
|
|
return Lang.format(mDateFmt, ["", 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";
|
|
}
|
|
|
|
// 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
|
|
// Clock, calendar and week-number fields.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function dateTime(id as Number, c as Ctx) as String? {
|
|
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(Settings.number("AltOffset1", 0), c); }
|
|
if (id == 865) { return offsetClock(Settings.number("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) + " " + dayOnly(c.greg); }
|
|
if (id == 851) { return dow(c.greg) + " " + monthDay(c.greg); }
|
|
if (id == 858) { return meridiem(c.greg); }
|
|
|
|
return null;
|
|
}
|
|
|
|
// Steps, calories, distance, floors and active minutes.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function activity(id as Number, c as Ctx) as String? {
|
|
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 has :activeMinutesDay) || 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 has :activeMinutesWeek) || 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); }
|
|
// Several ActivityMonitor.Info members compile everywhere but only
|
|
// exist on some devices and firmware -- the wheelchair counters in
|
|
// particular threw "Symbol Not Found" on a fenix 8. Check before use.
|
|
if (id == 5) {
|
|
return (i == null || !(i has :floorsDescended)) ? DASH
|
|
: num(i.floorsDescended);
|
|
}
|
|
if (id == 6) {
|
|
return (i == null || !(i has :metersClimbed)) ? DASH
|
|
: metres(i.metersClimbed, c);
|
|
}
|
|
if (id == 9) {
|
|
return (i == null || !(i has :pushDistance)) ? DASH
|
|
: dist(i.pushDistance, c);
|
|
}
|
|
if (id == 10) {
|
|
return (i == null || !(i has :pushes)) ? 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 || !(i has :timeToRecovery)) ? DASH
|
|
: num(i.timeToRecovery);
|
|
}
|
|
if (id == 11) {
|
|
return (i == null || !(i has :respirationRate)) ? DASH
|
|
: num(i.respirationRate);
|
|
}
|
|
if (id == 12) {
|
|
return (i == null || !(i has :stressScore)) ? DASH : num(i.stressScore);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Heart, stress, oxygen, body battery and profile-derived values.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function body(id as Number, c as Ctx) as String? {
|
|
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 || !(p has :restingHeartRate)) ? 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 has :weight) || 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 has :height) || 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; }
|
|
if (id == 278) {
|
|
return (p has :vo2maxRunning) ? num(p.vo2maxRunning) : DASH;
|
|
}
|
|
return (p has :vo2maxCycling) ? num(p.vo2maxCycling) : DASH;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Battery, alarms, notifications and radio state.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function system(id as Number, c as Ctx) as String? {
|
|
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");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Altitude, pressure, sun events and moon phase.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function environment(id as Number, c as Ctx) as String? {
|
|
if (id == 615) {
|
|
var a = c.act();
|
|
return (a == null || !(a has :altitude)) ? DASH : metres(a.altitude, c);
|
|
}
|
|
if (id == 616 || id == 733) {
|
|
var a = c.act();
|
|
if (a == null) { return DASH; }
|
|
var pa = null;
|
|
if (id == 616 && (a has :meanSeaLevelPressure)) {
|
|
pa = a.meanSeaLevelPressure;
|
|
} else if (id == 733 && (a has :ambientPressure)) {
|
|
pa = 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") + "%";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Garmin's cached conditions and today's forecast.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function weather(id as Number, c as Ctx) as String? {
|
|
var w = c.wx();
|
|
if (id == 306) { return w == null ? DASH : temp(w.temperature, c); }
|
|
if (id == 300) {
|
|
return (w == null || !(w has :feelsLikeTemperature)) ? DASH
|
|
: temp(w.feelsLikeTemperature, c);
|
|
}
|
|
if (id == 305) {
|
|
return (w == null || !(w has :relativeHumidity)) ? 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 has :windSpeed) || 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 || !(w has :windBearing)) ? DASH : num(w.windBearing);
|
|
}
|
|
if (id == 310) {
|
|
return (w == null || !(w has :windBearing)) ? DASH : windDir(w.windBearing);
|
|
}
|
|
if (id == 309) {
|
|
if (w == null || !(w has :observationTime) || 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; }
|
|
var hi = (f has :highTemperature) ? f.highTemperature : null;
|
|
var lo = (f has :lowTemperature) ? f.lowTemperature : null;
|
|
if (id == 301) { return temp(hi, c); }
|
|
if (id == 302) { return temp(lo, c); }
|
|
return temp(hi, c) + "/" + temp(lo, c);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Free text the wearer typed into the settings.
|
|
// Returns null when the id is not one of ours, so value()
|
|
// can try the next category.
|
|
function custom(id as Number, c as Ctx) as String? {
|
|
if (id == 703) { return Settings.text("Custom1"); }
|
|
if (id == 704) { return Settings.text("Custom2"); }
|
|
if (id == 705) { return Settings.text("Custom3"); }
|
|
return null;
|
|
}
|
|
|
|
// Text for one data slot. Categories are tried in turn rather
|
|
// than dispatched on an id range, because the ids interleave.
|
|
function value(id as Number, c as Ctx) as String {
|
|
var v = dateTime(id, c);
|
|
if (v == null) { v = activity(id, c); }
|
|
if (v == null) { v = body(id, c); }
|
|
if (v == null) { v = system(id, c); }
|
|
if (v == null) { v = environment(id, c); }
|
|
if (v == null) { v = weather(id, c); }
|
|
if (v == null) { v = custom(id, c); }
|
|
return v == null ? "" : v;
|
|
}
|
|
}
|