From 2fc3cee13926258243f86a791c2e175cc9ceb07b Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:27:53 -0800 Subject: [PATCH 01/18] Create app.js --- apps/pooqroman/app.js | 764 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 apps/pooqroman/app.js diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js new file mode 100644 index 000000000..a5de57210 --- /dev/null +++ b/apps/pooqroman/app.js @@ -0,0 +1,764 @@ +// pooqRoman +// +// Copyright (c) 2021 Stephen P Spackman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Notes: +// +// This only works for Bangle 2. + +////////////////////////////////////////////////////////////////////////////// +/* System integration */ + +const storage = require('Storage'); + +const settings = storage.readJSON("setting.json", true) || {}; + +const alarms = storage.readJSON('alarm.json', true) || []; + +/* + { on : true, + hr : 6.5, // hours + minutes/60 + msg : "Eat chocolate", + last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! + rp : true, // repeat + as : false, // auto snooze + timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes + } +*/ + +////////////////////////////////////////////////////////////////////////////// +/* Face-specific options */ + +class Options { + // Protocol: subclasses must have static id and defaults fields. + // Only fields named in the defaults will be saved. + constructor() { + this.id = this.constructor.id; + this.file = `${this.id}.opt`; + this.backing = storage.readJSON(this.file, true) || {}; + this.defaults = this.constructor.defaults; + Object.keys(this.defaults).forEach(k => this.bless(k)); + } + + writeBack(delay) { + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout( + () => { + this.timeout = null; + storage.writeJSON(this.file, this.backing); + }, + delay + ); + } + + bless(k) { + Object.defineProperty(this, k, { + get: () => this.backing[k] == null ? this.defaults[k] : this.backing[k], + set: v => { + this.backing[k] = v; + // Ten second writeback delay, since the user will roll values up and down. + this.writeBack(10000); + return v; + } + }); + } + + showMenu(m) { + if (m) { + for (const k in m) if ('init' in m[k]) m[k].value = m[k].init(); + m[''].selected = -1; // Workaround for self-selection bug. + } + E.showMenu(m); + } + + reset() { + this.backing = {}; + this.writeBack(0); + } + + interact() {this.showMenu(this.menu);} +} + +class RomanOptions extends Options { + constructor() { + super(); + this.menu = { + '': {title: '* face options *'}, + '< Back': _ => {this.showMenu(); this.emit('done');}, + Ticks: { + init: _ => this.resolution, + min: 0, max: 3, + onchange: x => this.resolution = x, + format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x] + }, + 'Display': { + init: _ => this.o24h == null ? 0 : 1 + this.o24h, + min: 0, max: 2, + onchange: x => this.o24h = [null, 0, 1][x], + format: x => ['system', '12h', '24h'][x] + }, + 'Day of Week': { + init: _ => this.dow, + onchange: x => this.dow = x + }, + Calendar: { + init: _ => this.calendric, + min: 0, max: 2, + onchange: x => this.calendric = x, + format: x => ['none', 'day', 'date'][x] + }, + Defaults: _ => {this.reset();} + }; + } +} + +RomanOptions.id = 'pooqroman'; + +RomanOptions.defaults = { + resolution: 1, + dow: true, + calendric: 2, + o24h: !settings["12hour"], + bg: g.theme.bg, + fg: g.theme.fg, + barBg: g.theme.fg, + barFg: g.theme.bg, + hourFg: g.theme.fg, + minuteFg: g.theme.fg, + secondFg: g.theme.fg2, + rectFg: g.theme.fg, + hubFg: g.theme.fg, + alarmFg: '#f00', + timerFg: '#0f0', + active: g.theme.fg2, +}; + +////////////////////////////////////////////////////////////////////////////// +/* Assets (generated by resourcer.js, in this directory) */ + +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +const romanPartsF = [ + dec( + 'wEBsEB3//7//9//+0AjUAguAg3AgYQJjfAgv+gH/8Fg/0gh/AgP4gf2h/j/+BCAP' + + 'wgFggEggEQgEMgEHwEDEIIyDuED3kD7+H9vn2k/hEPgMP4Xevd+j4QB7kA9kAmkA' + + 'hUGgOH8Hn3le4+GgH32PuvfGj+CCAMDgXD4dz+evt9DgcL7fXn87h8NCAMP+Ef/0' + + 'eg+egPugF2j0bCAPAh3wh88h8P/8BNwI' + ), 97, dec('gUDgUGgUJgYFBhsBhMJhgA=='), 17 +];const fontF = [ + dec( + 'AAUwAIM/4F/8HguHAmABBAoIJBBoIUBkEwsEw//wAIIdDBoUQBoIfC+HB+Hj2F/m' + + 'E+CIXAoHEsHMuHcmH8mHuuHH8GBGIUAwEBwEHwH/wH5+EBAIILCCAP8oH8EYXMmA' + + 'BB5wjCgYjCAYMP8E+uF8mHsCIWHCIgCBAIXw4fw54tBgBsBGgUAnKLC99w40wAII' + + 'FBBIINBCIM8gF+iHnmHDuHD8HnDYMAjizEMYJJBn+A+OAAYIHBBYKjDXYKvDYZYP' + + 'D40AAIYMBZYgkC4Hg4DnDuH/8H/BYIVCv/wnEAjwBCAoIJBEIYRFh0Ag8AgPAEYQ' + + 'RCJIJNBfYRXKnFAvlg9ihE8dwsfgkLFHMYgJF8DNCh+AUYWAA4ILBAAJGB/4PB+D' + + '9CgADCEoIPCJobbBB4IBBAoJdDEgXggvwhuwAIcH8EDRIh/BhkwAIMOuAPCMYQDB' + + 'A4ILBCIcGsECoAPLU4oPDH42ggeAB4XEg/mh1zhkzh03g/+h/4J4nwg0AhjbDRII' + + 'vCt/wAIIVFAoKTBCYIXBDIYHHEIYVFGJJxHSI8P/8H/6hLF44BBM4IABg8gh6NEh' + + 'vwgngBoITBv/Av7PBV4kAsArCfYIVBuEABYNwA4I3BD4cPL4UAM4IXBBYQfC4kP8' + + '0AucAmcAu8PXogA=' + ), 32, dec('gINMgUAhMHhIAGCQ0KAQIKBgwEBgcIBAQVEhIJBhAeIBQIADAoUDEQULBQcHg4FD' + + 'CII='), 16 +]; +const lockI = dec('iMSwMAgfwgf8geHgeB4PA8HguFwnH//9//+4gPf//v//3gE7//9//+8EHCAO///A'); +const batteryI = dec('h8SwMAgPggfAv/4//x//j//H/+P/8f/0//gOOA=='); +const chargeI = dec('h0MwIEBkEBwEMgFwgeAj/w/+AjkA8EDgEYgFAA=='); +const GPSI = dec('iUQwMAhEAgsAgUggFEgEKvEBn0Aj+AgfgglygsJosgxNGiNIgWJ4FBEoM4gA'); +const HRMI = dec('iMRwMAnken8fzfd7v+/3/v9/38/z+b5tiiM3/eP/+D/+AAIM/wEPwEDwEAAIIA=='); +const compassI = dec('iMRwMAgfgg/8g8ng0Q40ImcOjcHg+DwfB4Ph2Hw7FsolmkUxwEwuFwj/wEIMAA=='); + +////////////////////////////////////////////////////////////////////////////// +/* Squeezable strings */ + +class Formattable { + width(g) {return this.w != null ? this.w : (this.w = g.stringWidth(this.text));} + print(g, x, y) {g.drawString(this.text, x, y); return this.width();} +} + +class Fixed extends Formattable { + constructor(text) { + super(); + this.text = text; + } + squeeze() {return false;} +} + +class Squeezable extends Formattable { + constructor(named, index) { + super(); + this.named = named; + this.index = index; + this.end = index + named.forms; + } + squeeze() { + if (this.index >= this.end) return false; + this.index++; + this.w = null; + return true; + } + get text() {return this.named.table[this.index];} +} + +class Named { + constructor(forms, table) { + this.forms = forms; + this.table = table; + } + on(index) {return new Squeezable(this, this.forms * index);} +} + +////////////////////////////////////////////////////////////////////////////// +/* Face */ + +// Static geometry +const barW = 26, barH = g.getHeight(), barX = g.getWidth() - barW, barY = 0; +const faceW = g.getWidth() - barW, faceH = g.getHeight(); +const faceX = 0, faceY = 0, faceCX = faceW / 2, faceCY = faceH / 2; +const rectX = faceX + 35, rectY = faceY + 24, rectW = 80, rectH = 128; + +// Extended-Roman-numeral labels +const layout = E.toUint8Array( + 75, 23, // XII + 132, 24, // I + 132, 61, // II + 132, 97, // III + 132, 133, // IV + 132, 170, // V + 75, 171, // VI + 18, 170, // VII + 18, 133, // VIII + 18, 97, // IX + 18, 61, // X + 18, 24 // XI +); + +const numeral = (n, options) => [ + 'n', // 0 + 'abc', // I + 'abdc', // II + 'abddc', // III + 'abefg', // IV + 'hfg', // V + 'hfibc', // VI + 'hfibdc', // VII + 'hfibddc', // VIII + 'abjk', // IX + 'kjk', // X + 'kjbc', // XI + 'kjbdc', // XII + 'kjbddc', // XIII + 'kjbefg', // XIV + 'kjefg', // XV + 'labc', // XVI + 'labdc', // XVII + 'labddc', // XVIII + 'kjbjk', // XIX + 'kjjk', // XX + 'mabc', // XXI + 'mabdc', // XXII + 'mabddc', // XXIII +][options.o24h ? n % 24 : (n + 11) % 12 + 1]; + +const formatMonth = new Named(4, [ + 'January', 'Jan.', 'Jan', 'I', + 'February', 'Feb.', 'Feb', 'II', + 'March', 'Mar.', 'Mar', 'III', + 'April', 'Apr.', 'Apr', 'IV', + 'May', 'May', 'May', 'V', + 'June', 'June', 'Jun', 'VI', + 'July', 'July', 'Jul', 'VII', + 'August', 'Aug.', 'Aug', 'VIII', // VIII *is* narrower than Aug, our I is thin. + 'September', 'Sept.', 'Sep', 'IX', + 'October', 'Oct.', 'Oct', 'X', + 'November', 'Nov.', 'Nov', 'XI', + 'December', 'Dec.', 'Dec', 'XII' +]); +const formatDom = { + on: d => new Fixed(d.toString()) +}; +const formatDow = new Named(4, [ + 'Sunday', 'Sun.', 'Sun', 'Su', + 'Monday', 'Mon.', 'Mon', 'M', + 'Tuesday', 'Tues.', 'Tue', 'Tu', + 'Wednesday', 'Weds.', 'Wed', 'W', + 'Thursday', 'Thurs.', 'Thu', 'Th', + 'Friday', 'Fri.', 'Fri', 'F', + 'Saturday', 'Sat.', 'Sat', 'Sa' +]); + +const isString = x => typeof x == 'string'; +const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; +const imageHeight = i => isString(i) ? i.charCodeAt(1) : i.height; + +const events = { + // Items are {time: number, wall: boolean, priority: number, + // past: bool, future: bool, precision: number, + // colour: colour, dramatic?: bool, event?: any} + fixed: [{time: Number.POSITIVE_INFINITY}], // indexed by ms absolute + wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms + + clean: function(now, l) { + let o = now.getTimezoneOffset() * 60000; + let tf = now.getTime() + l, tw = tf - o; + // Discard stale events: + while (this.wall[0].time <= tw) this.wall.shift(); + while (this.fixed[0].time <= tf) this.fixed.shift(); + }, + + scan: function(now, from, to, f) { + result = Infinity; + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let c, p, i, l = from - o, h = to - o; + for (i = 0; (c = this.wall[i]).time < l; i++) ; + for (; (c = this.wall[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(result, f(c, new Date(c.time + o), p)); + } + l += o; h += o; t += o; + for (i = 0; (c = this.fixed[i]).time < l; i++) ; + for (; (c = this.fixed[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(f(c, new Date(c.time), p)); + } + return result; + }, + + span: function(now, width) { + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let lfence = [], rfence = []; + this.scan(now, now - width, now + width, (e, d, p) => { + if (p) { + for (let j = 0; j <= e.priority; j++) { + if (e.time < (lfence[e.priority] || t)) lfence[e.priority] = e.time; + } + } else { + for (let j = 0; j <= e.priority; j++) { + if (e.time > (rfence[e.priority] || t)) rfence[e.priority] = e.time; + } + } + }); + for (let j = 0; ; j += 0.5) { + if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) { + return [lfence[Math.floor(j)] || t, rfence[Math.ceil(j)] || t]; + } + } + }, + + insert: function(t, wall, e) { + let v = wall ? this.wall : this.fixed; + e.time = t = t - (wall ? t.getTimezoneOffset() * 60000 : 0); + v.splice(v.findIndex(x => x.time > t), 0, e); + }, + + loadFromSystem: function(options) { + alarms.forEach(x => { + if (x.on) { + const t = new Date(); + let h = x.hr; + let m = h % 1 * 60; + let s = m % 1 * 60; + let ms = s % 1 * 1000; + t.setHours(h - h % 1, m - m % 1, s - s % 1, ms); + // There's a race condition here, but I'm not sure what we can do about it. + if (t < Date.now() || x.last === t.getDate()) t.setDate(t.getDate() + 1); + this.insert(t, true, { + priority: 0, + past: false, // System alarms seem uninteresting if past? + future: true, + precision: x.timer ? 1000 : 60000, + colour: x.timer ? options.timerFg : options.alarmFg, + event: x + }); + } + }); + return this; + }, +}; + +////////////////////////////////////////////////////////////////////////////// +/* The main face logic */ + +class Sidebar { + constructor(g, x, y, w, h, options) { + this.g = g; + this.options = options; + this.x = x; + this.y = this.initY = y; + this.h = h; + this.rate = Infinity; + this.doLocked = Sidebar.status(_ => Bangle.isLocked(), lockI); + this.doHRM = Sidebar.status(_ => Bangle.isHRMOn(), HRMI); + this.doGPS = Sidebar.status(_ => Bangle.isGPSOn(), GPSI, Sidebar.gpsColour(options)); + } + reset(rate) {this.y = this.initY; this.rate = rate; return this;} + print(t) { + this.y += 4 + t.print( + this.g.setColor(this.options.barFg).setFontAlign(-1, 1, 1), + this.x + 3, this.y + 4 + ); + return this; + } + pad(n) {this.y += n; return this;} + free() {return this.h - this.y;} + static status(p, i, c) { + return function() { + if (p()) { + this.g.setColor(c ? c() : this.options.barFg) + .drawImage(i, this.x + 4, this.y += 4); + this.y += imageHeight(i); + } + return this; + }; + } + static gpsColour(o) { + const fix = Bangle.getGPSFix(); + return fix && fix.fix ? o.active : o.barFg; + } + doPower() { + const c = Bangle.isCharging(); + const b = E.getBattery(); + if (c || b < 50) { + let g = this.g, x = this.x, y = this.y, options = this.options; + g.setColor(options.barFg).drawImage(batteryI, x + 4, y + 4); + g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0'); + const h = 13 * (100 - b) / 100; + g.fillRect(x + 8, y + 7 + h, x + 17, y + 20); + // Espruino disallows blank leading rows in icons, for some reason. + if (c) g.setColor(options.barBg).drawImage(chargeI, x + 4, y + 8); + this.y = y + imageHeight(batteryI) + 4; + } + return this; + } + doCompass() { + if (Bangle.isCompassOn()) { + const c = Bangle.getCompass(); + const a = c && this.rate <= 1000; + this.g.setColor(a ? this.options.active : this.options.barFg).drawImage( + compassI, + this.x + 4 + imageWidth(compassI) / 2, + this.y + 4 + imageHeight(compassI) / 2, + a ? {rotate: c.heading / 180 * Math.PI} : undefined + ); + this.y += 4 + imageHeight(compassI); + } + return this; + } +} + +class Roman { + constructor(g, events) { + this.g = g; + this.state = {}; + const options = this.options = new RomanOptions(); + this.events = events.loadFromSystem(this.options); + this.timescales = [1000, [1000, 60000], 60000, 3600000]; + this.sidebar = new Sidebar(g, barX, barY, barW, barH, options); + this.hours = Roman.hand(g, 3, 0.5, 12, _ => options.hourFg); + this.minutes = Roman.hand(g, 2, 0.9, 60, _ => options.minuteFg); + this.seconds = Roman.hand(g, 1, 0.9, 60, _ => options.secondFg); + } + + reset() {this.state = {}; this.g.clear(true);} + + doIcons(which) {this.state.iconsOk = null;} + + // Watch hands. These could be improved, graphically. + // If we restricted them to 60 positions, we could feasibly hand-draw them? + static hand(g, w, l, d, c) { + return p => { + g.setColor(c()); + p = ((12 * p / d) + 1) % 12; + let h = l * rectW / 2; + let v = l * rectH / 2; + let poly = + p <= 2 ? [faceCX + w, faceCY, faceCX - w, faceCY, + faceCX + h * (p - 1), faceCY - v, + faceCX + h * (p - 1) + 1, faceCY - v] + : p < 6 ? [faceCX + 1, faceCY + w, faceCX + 1, faceCY - w, + faceCX + h, faceCY + v / 2 * (p - 4), + faceCX + h, faceCY + v / 2 * (p - 4) + 1] + : p <= 8 ? [faceCX - w, faceCY + 1, faceCX + w, faceCY + 1, + faceCX - h * (p - 7), faceCY + v, + faceCX - h * (p - 7) - 1, faceCY + v] + : [faceCX, faceCY - w, faceCX, faceCY + w, + faceCX - h, faceCY - v / 2 * (p - 10), + faceCX - h, faceCY - v / 2 * (p - 10) - 1]; + g.fillPoly(poly); + }; + } + + static pos(p, r) { + let h = r * rectW / 2; + let v = r * rectH / 2; + p = (p + 1) % 12; + return p <= 2 ? [faceCX + h * (p - 1), faceCY - v] + : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)] + : p <= 8 ? [faceCX - h * (p - 7), faceCY + v] + : [faceCX - h, faceCY - v / 2 * (p - 10)]; + } + + alert(e, date, now, past) { + const g = this.g; + g.setColor(e.colour); + const dt = date - now; + if (e.precision < 60000 && dt >= 0 && e.future && dt <= 59000) { // Seconds away + const p = Roman.pos(date.getSeconds() / 5, 0.95); + g.drawLine(faceCX, faceCY, p[0], p[1]); + return 1000; + } else if (e.precision < 3600000 && dt >= 0 && e.future && dt <= 3540000) { // Minutes away + const p = Roman.pos(date.getMinutes() / 5 + date.getSeconds() / 300, 0.8); + g.drawLine(p[0] - 5, p[1], p[0] + 5, p[1]); + g.drawLine(p[0], p[1] - 5, p[0], p[1] + 5); + return dt < 119000 ? 1000 : 60000; // Turn on second hand two minutes up. + } else if (e.precision < 43200000 && dt >= 0 ? e.future : e.past) { // Hours away + const p = Roman.pos(date.getHours() + date.getMinutes() / 60, 0.6); + const poly = [p[0] - 4, p[1], p[0], p[1] - 4, p[0] + 4, p[1], p[0], p[1] + 4]; + if (date >= now) g.fillPoly(poly); + else g.drawPoly(poly, true); + return 3600000; + } + return Infinity; + } + + render(d, rate) { + const g = this.g; + const state = this.state; + const options = this.options; + const events = this.events; + events.clean(d, -39600000); // 11h + + // Sidebar: icons and date + if (d.getDate() !== state.date || !state.iconsOk) { + const sidebar = this.sidebar; + state.date = d.getDate(); + state.iconsOk = true; + g.setColor(options.barBg).fillRect(barX, barY, barX + barW, barY + barH); + + sidebar.reset(rate).doLocked().doPower().doGPS().doHRM().doCompass(); + g.setFontCustom.apply(g, fontF); + let formatters = []; + let month, dom, dow; + if (options.calendric > 1) { + formatters.push(month = formatMonth.on(d.getMonth())); + } + if (options.calendric > 0) { + formatters.push(dom = formatDom.on(d.getDate())); + } + if (options.dow) { + formatters.push(dow = formatDow.on(d.getDay())); + } + // Obnoxiously inefficient iterative method :( + let ava = sidebar.free() - 3, use, i = 0, j = 0; + while ((use = formatters.reduce((l, f) => l + f.width(g) + 4, 0)) > ava && + j < formatters.length) + for (j = 0; + !formatters[i++ % formatters.length].squeeze() && + j < formatters.length; + j++) ; + if (dow) sidebar.print(dow); + sidebar.pad(ava - use); + if (month) sidebar.print(month); + if (dom) sidebar.print(dom); + } + + // Hour labels and (purely aesthetic) box; clear inner face. + let keyHour = d.getHours() < 12 ? 1 : 13; + let alertSpan = events.span(d, 43200000); + let l = Math.floor(alertSpan[0] / 3600000) % 24; + let h = Math.ceil(alertSpan[1] / 3600000) % 24; + if ((l - keyHour + 24) % 24 >= 12) keyHour = l; + else if ((h - keyHour + 24) % 24 >= 12) keyHour = (h + 13) % 24; + if (keyHour !== state.keyHour) { + state.keyHour = keyHour; + g.setColor(options.bg) + .fillRect(faceX, faceY, faceX + faceW, faceY + faceH) + .setFontCustom.apply(g, romanPartsF) + .setFontAlign(0, 1) + .setColor(options.fg); + // In order to deal with timezone changes more logic will be required, + // since the labels may be in unusual locations (even offset when + // a non-integral zone is involved). The value of keyHour can be + // anything in [hr-12, hr] mod 24. + for (let h = keyHour; h < keyHour + 12; h++) { + g.drawString( + numeral(h % 24, options), + faceX + layout[h % 12 * 2], + faceY + layout[h % 12 * 2 + 1] + ); + } + g.setColor(options.rectFg) + .drawRect(rectX, rectY, rectX + rectW - 1, rectY + rectH - 1); + } else { + g.setColor(options.bg) + .fillRect(rectX + 1, rectY + 1, rectX + rectW - 2, rectY + rectH - 2) + .setColor(options.fg); + } + + // Alerts + let b = new Date(d.getTime()); + b.setHours(keyHour, 0, 0, 0); + if (b > d) b.setDate(b.getDate() - 1); + let requestedRate = events.scan( + d, b, b + 43200000, (e, t, p) => this.alert(e, t, d, p) + ); + if (rate > requestedRate) rate = requestedRate; + + // Hands + // Here we are using incremental hands for hours and minutes. + // If we quantised, we could use hand-crafted bitmaps, though. + this.hours(d.getHours() + d.getMinutes() / 60); + if (rate < 3600000) { + this.minutes(d.getMinutes() + d.getSeconds() / 60); + } + if (rate < 60000) this.seconds(d.getSeconds()); + g.setColor(options.hubFg).fillCircle(faceCX, faceCY, 3); + return requestedRate; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Master clock */ + +class Clock { + constructor(face) { + this.face = face; + this.timescales = face.timescales; + this.options = face.options; + this.rates = {}; + + this.options.on('done', () => this.start()); + + this.listeners = { + lcdPower: on => on ? this.active() : this.inactive(), + charging: () => {face.doIcons('charging'); this.active();}, + lock: () => {face.doIcons('locked'); this.active();}, + faceUp: up => {this.conservative = !up; this.active();}, + drag: e => { + if (this.t0) { + if (e.b) { + e.x > this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x); + e.y > this.yN && (this.yN = e.y) || e.y > this.yX && (this.xY = e.y); + } else if (this.xX - this.xN < 20) { + if (e.y - this.e0.y < -50) { + this.options.resolution > 0 && this.options.resolution--; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (e.y - this.e0.y > 50) { + this.options.resolution < this.timescales.length - 1 && + this.options.resolution++; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) { + this.stop(); + this.options.interact(); + } + this.t0 = null; + } + } else if (e.b) { + this.t0 = Date.now(); this.e0 = e; + this.xN = this.xX = e.x; this.yN = this.yX = e.y; + } + } + }; + } + + redraw(rate) { + const now = this.updated = new Date(); + if (this.refresh) this.face.reset(); + this.refresh = false; + rate = this.face.render(now, rate); + if (rate !== this.rates.face) { + this.rates.face = rate; + this.active(); + } + return this; + } + + inactive() { + this.timeout && clearTimeout(this.timeout); + this.exception && clearTimeout(this.exception); + this.interval && clearInterval(this.interval); + this.timeout = this.exception = this.interval = this.rate = null; + return this; + } + + active() { + const prev = this.rate; + const now = Date.now(); + let rate = Infinity; + for (const k in this.rates) { + let r = this.rates[k]; + r === +r || (r = r[+this.conservative]) + r < rate && (rate = r); + } + const delay = rate - now % rate + 1; + this.refresh = true; + + if (rate !== prev) { + this.inactive(); + this.redraw(rate); + if (rate < 31622400000) { // A year! + this.timeout = setTimeout( + () => { + this.inactive(); + this.interval = setInterval(() => this.redraw(rate), rate); + if (delay > 1000) this.redraw(rate); + this.rate = rate; + }, delay + ); + } + } else if (rate > 1000) { + if (!this.exception) this.exception = setTimeout(() => { + this.redraw(rate); + this.exception = null; + }, this.updated + 1000 - Date.now()); + } + return this; + } + + stop() { + this.inactive(); + for (const l in this.listeners) { + Bangle.removeListener(l, this.listeners[l]); + } + return this; + } + + start() { + this.inactive(); // Reset to known state. + this.conservative = false; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + for (const l in this.listeners) { + Bangle.on(l, this.listeners[l]); + } + Bangle.setUI('clock'); + return this; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Main */ + +const clock = new Clock(new Roman(g, events)).start(); From ad96a97ae9fc12d8d93dceb60d1c861ce60ad630 Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:34:25 -0800 Subject: [PATCH 02/18] Create app-icon.js --- apps/pooqroman/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/pooqroman/app-icon.js diff --git a/apps/pooqroman/app-icon.js b/apps/pooqroman/app-icon.js new file mode 100644 index 000000000..20a9c8b0a --- /dev/null +++ b/apps/pooqroman/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIAWiEAgIpKEwgrFgAaBgIcBAAwREC4oVBBoQoCAQoXJBogXqI653DC6SnEC9RHXX/6/kSgIAGU5wAICQhfGACAX/C/4AOXIIX/C/4X/C/4XUgEBF6wYHI6AYGL6MACIgXRCIISDR6QYEU6YYDX6gYCAAKxHDB4XTDAYXUL6oAgA==")) From 8ff08cbaab00ce915eac1cf66ac2af4aaee220d9 Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:35:39 -0800 Subject: [PATCH 03/18] Create resourcer.js --- apps/pooqroman/resourcer.js | 721 ++++++++++++++++++++++++++++++++++++ 1 file changed, 721 insertions(+) create mode 100644 apps/pooqroman/resourcer.js diff --git a/apps/pooqroman/resourcer.js b/apps/pooqroman/resourcer.js new file mode 100644 index 000000000..c172812c7 --- /dev/null +++ b/apps/pooqroman/resourcer.js @@ -0,0 +1,721 @@ +// pooqRoman resource maker +// +// Copyright (c) 2021 Stephen P Spackman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Notes: +// +////////////////////////////////////////////////////////////////////////////// +/* ==ASSETS== */ + +const heatshrink = require('heatshrink'); + +const enc = x => { + const d = btoa(require("heatshrink").compress(x)); + var r = "'" + d.substr(0, 64); + for (let i = 64; i < d.length; i += 64) r += "' +\n '" + d.substr(i, 64); + return r + "'"; +}; + +const prepBitmap = (name, data) => { + const image = Graphics.createImage(data); + const raw = String.fromCharCode(image.width, image.height, 0x81, 0) + image.buffer; + const x = ` +const ${name}I = dec(${enc(raw)}); +`; + return x; +}; + +const prepFont = (name, data) => { + const image = Graphics.createImage(data); + const lengths = Uint8Array(256); + const offsets = Uint16Array(256); + const adjustments = Uint16Array(256); + let min = Infinity, max = -Infinity; + const lines = data.split('\n'); + let m; + // This regexp is clearly suboptimal, but Espruino's regexp engine is really wonky + // and doesn't process nested parentheses or alternation correctly. + for (let i = 0; i < 5 && !(m = /^(<*)=([*\d]+)(=*)(>*)$/.exec(lines[i])); i++); + if (!m) throw new Error('Missing or incorrect header'); + const desc = m[1].length, body = 1 + m[2].length + m[3].length, asc = m[4].length; + const h = desc + body + asc; + let width = m[2] == '*' ? null : +m[2]; + let c = null, o = 0; + lines.forEach((line, l) => { + if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { + const h = m[2] == '='; + if (m[1].length > desc || h && m[1].length != desc) + throw new Error('Invalid descender height at ' + l); + if (m[2].length + m[3].length + m[4].length != body) + throw new Error('Invalid body height at ' + l); + if (m[5].length > asc || h && m[5].length != asc) + throw new Error('Invalid ascender height at ' + l); + if (c != null) { + lengths[c] = l - o; + if (width !== null && width !== lengths[c]) + throw new Error( + `Character has width ${lengths[c]} != ${width} at ${offsets[c]}` + ); + c = null + } + if (!h) { + c = m[3].charCodeAt(0); + if (c < min) min = c; + if (c > max) max = c; + o = l + 1; + offsets[c] = l; + adjustments[c] = m[1].length + } + } + }); + const xoffs = Uint8Array(lines.length); + const ypos = Uint16Array(lines.length); + ypos.fill(0xffff); + const w0 = lengths[min]; + let widths = ''; + for (c = min, o = 0; c <= max; c++) { + for (i = 0, j = offsets[c]; i < lengths[c]; i++) { + xoffs[j] = asc + body + adjustments[c] - 1; + ypos[j++] = o++; + } + widths += String.fromCharCode(lengths[c]); + } + const raster = Graphics.createArrayBuffer(h, o, 1, {msb: true}); + const writer = Graphics.createCallback( + image.width, image.height, 1, + (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col) + ); + writer.drawImage(image); + if (width === null) width = `dec(${enc(widths)})`; + const x = `const ${name}F = [ + dec( + ${enc(raster.buffer)} + ), ${min}, ${width}, ${h} +];`; + return x; +}; + +res = ` +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +`; + +res += prepFont('romanParts', ` +<=*============== +-a-------------- +x x +xx xx +-b-------------- +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +-c-------------- +xx xx +x x +-d-------------- +xx xx +xx xx +xx xx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +-e-------------- +xx xx +x xxxx +<-f-------------- + xxxxxxxx + xxxxxxxxxxx + xxxxxxx xx + xxxxxx x +xxxxx + xxxxxx x + xxxxxxx xx + xxxxxxxxxxx + xxxxxxxx +-g-------------- + xxxx + xx + x +-h-------------- + x + xx + xxxx +-i-------------- +x xxxx +xx xx +-j-------------- +xx xx +xxx xxx +xxxx xxxx +xxxxxx xxxxxx +xx xxxx xxxx xx +x xxxxxx x + xxxx +x xxxxxx x +xx xxxx xxxx xx +xxxxxx xxxxxx +xxxx xxxx +xxx xxx +xx xx +-k-------------- +x x +<-l-------------- + xx x + xxxxxx xx + xxxx xxxx xxx + xxxx xx xxxx x +xxx xx + xxxx xx xxxx x + xxxx xxxx xxx + xxxxxx xx + xx x +-m-------------- +x xx x +xx xxxx xx +xxx xxxxxx xxx +x xxxx xx xxxx x + xx xx +x xxxx xx xxxx x +xxx xxxxxx xxx +xx xxxx xx +x xx x +-n-------------- + xxxxxxxx + xxxxxxxxxxxx + xxxx xxxx +xxxx xxxx +xxx xxx +xx xxxx xx +xx xxxx xx +xxx xxx +xxxx xxxx + xxxx xxxx + xxxxxxxxxxxx + xxxxxxxx +<=*============== +`); + +res += prepFont('font', ` +<<<<=*======>>>> +- ------ + +-.------ +xx +xx +-0------>>>> + xxxxxxxx + xxxxxxxxxx +xxx xxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-1------>>>> +xx x +xx xx +xxxxxxxxxxxx +xxxxxxxxxxxx +xx +xx + +-2------>>>> +x x +xx xx +xxx xxx +xxxx xx +xxxxx xx +xx xxx xxx +xx xxxxxxx +xx xxxxx + +-3------>>>> + x xx + xx x xx +xxx xx xx +xx xxx xx +xx xxxxxx +xxx xxx xxx + xxxxxx xx + xxx x + +-4------>>>> + x + xx + xxxx + xxxxxxxxx +xxxxx xxxxx +xxxxx + xx + xx + +-5------>>>> + x xxxxxx + xx xxxxxx +xxx xx xx +xx xx xx +xx xx xx +xxx xxx xx + xxxxxx xx + xxxx + +-6------>>>> + xxxx + xxxxxxx +xxx xxxxx +xx xxxxx +xx xx xxx +xxx xxx xx + xxxxxx x + xxxx + +-7------>>>> + xx + xx +xxxx xx +xxxxxx xx + xxxx xx + xxxxxx + xxxx + x + +-8------>>>> + xxx xxx + xxxxxxxxxx +xxx xxxx xxx +xx xx xx +xx xx xx +xxx xxxx xxx + xxxxxxxxxx + xxx xxx + +-9------>>>> + xxxx +x xxxxxx +xx xxx xxx +xxx xx xx + xxxxx xx + xxxxx xxx + xxxxxxx + xxx + +-A------>>>> +xx +xxxxx + xxxxxxx + xxxxxxx + xx xxxx + xxxxxxx + xxxxxxx +xxxxx +xx + +-D------>>>> +xx xx +xxxxxxxxxxxx +xxxxxxxxxxxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-F------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + xx xx + xx xx + xx xx + xx +-I------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + +-J------>>>> + xx + xxx xx +xxx xx +xx xx +xxx xx + xxxxxxxxxxx + xxxxxxxxxx + xx +-M------>>>> +xxxxxxxxxxxx +xxxxxxxxxxx + xxx + xxxx + xxxx + xxx +xxxxxxxxxxx +xxxxxxxxxxxx + +-N------>>>> +xxxxxxxxxxxx +xxxxxxxxxxx + xxx + xxx + xxx + xxx + xxxxxxxxxxx +xxxxxxxxxxxx + +-O------>>>> + xxxxxxxx + xxxxxxxxxx +xxx xxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-S------>>>> + x xxx + xx xxxxx +xxx xx xxx +xx xx xx +xx xx xx +xxx xx xxx + xxxxx xx + xxx x + +-T------>>>> + xx + xx + xx +xxxxxxxxxxxx +xxxxxxxxxxxx + xx + xx + xx +-V------>>>> + xxx + xxxxxx + xxxxx +xxxxx + xxxxx + xxxxxx + xxx + +-W------>>>> + xxxx + xxxxxxxx +xxxxxxxx + xxxx + xxxx + xxxx +xxxxxxxx + xxxxxxxx + xxxx +-X------>>>> +xx xx +xxx xxx + xxx xxx + xxxx + xxxx + xxx xxx +xxx xxx +xx xx + +-a------ + xxx +xxxxx x +xx xx xx +xx xx xx +xx xx xx + xxxxxx +xxxxxx + +-b------>>>> +xxxxxxxxxxxx + xxxxxxxxxxx +xx xx +xx xx +xxx xxx + xxxxxx + xxxx + +-c------ + xxxx + xxxxxx +xxx xxx +xx xx +xx xx + xx xx + x x + +-d------>>>> + xxxx + xxxxxx +xxx xxx +xx xx +xx xx + xxxxxxxxxxx +xxxxxxxxxxxx + +-e------ + xxxx + xxxxxx +xx xx xx +xx xx xx +xx xx xx + x xxxx + xxx + +<<<<-g------ + x xxxx + xx xxxxxx +xx xxx xxx +xx xx xx +xxx xx xxx + xxxxxxxxxx + xxxxxxxxx + +-h------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + xx + xx + xxx +xxxxxxx +xxxxxx + +-i------>>>> +xxxxxxxx xx +xxxxxxxx xx + +-l------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + +-m------ +xxxxxxxx +xxxxxxx + xxx + xxx +xxxxxxx +xxxxxxx + xxx + xxx +xxxxxxx +xxxxxx + +-n------ +xxxxxxxx +xxxxxxx + xxx + xx + xxx +xxxxxxx +xxxxxx + +-o------ + xxxx + xxxxxx +xxx xxx +xx xx +xxx xxx + xxxxxx + xxxx + +<<<<-p------ +xxxxxxxxxxxx +xxxxxxxxxxx + xx xx + xx xx + xxx xxx + xxxxxx + xxxx + +-r------ +xxxxxxxx +xxxxxxx + xxx + xx + xx + xx + +-s------ + x xxx +xx xxxxx +xx xx xx +xx xx xx +xxxxx xx + xxx x + +-t------>>>> + xx + xxxxxxxxx + xxxxxxxxxx +xxx xx +xx xx +xx xx + xx + +-u------ + xxxxxx + xxxxxxx +xxx +xx +xxx + xxxxxxx +xxxxxxxx + +-v------ + xx + xxxx + xxxx +xxxx + xxxx + xxxx + xx + +<<<<-y------ + x xxxxxx + xx xxxxxxx +xx xxx +xx xx +xxx xxx + xxxxxxxxxxx + xxxxxxxxx + +<<<<=*======>>>> +`); + +res += prepBitmap('lock', ` + xxxxxx + xxxxxxxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx +`); + +res += prepBitmap('battery', ` + xxxx + xxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx +`); + +res += prepBitmap('charge', ` + x + xx + xx + xxx + xxx + xxxxxxxxx + xxxxxxxxx + xxx + xxx + xx + xx + x +`); + +res += prepBitmap('GPS', ` + x + x x + x x + x x + x x xxxx + x xxxxx + xxxxxx + xxxxx + x xxx x + x x x x x + x x x x x + x x xx x x + x x x x + x xxx x + x + xxx +`); + +res += prepBitmap('HRM', ` + xxxx xxxx + xxxxxx xxxxxx + xx xxxx xxx xxx +xxx xxxxxxxx xxxx +xxx xxxxxxxx xxxx +xxx xxxxxxxx xxxx +xx xxxxxxx xxxx +xx xx xxxx xx x + xx x x x + xx xxxxxxxx xxx + xxxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxx + xxxxx + xxx + x +`); + +res += prepBitmap('compass', ` + xxxxx + xxxxxxxxx + xxx x xxx + xx x xx + xx x xx + xx xxx xx +xx xxx xx +xx xxx xx +xx xxx xx +xx xx xx xx +xx xx xx xx + xx x x xx + xx x x xx + xx xx + xxx xxx + xxxxxxxxx + xxxxx +`); + +print(res); From 4803365ab3f8a6b3e2f9a6f650b642326a952010 Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:49:28 -0800 Subject: [PATCH 04/18] Update app.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conform to filename conventions for our settings file—it is json. --- apps/pooqroman/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js index a5de57210..8da638159 100644 --- a/apps/pooqroman/app.js +++ b/apps/pooqroman/app.js @@ -52,7 +52,7 @@ class Options { // Only fields named in the defaults will be saved. constructor() { this.id = this.constructor.id; - this.file = `${this.id}.opt`; + this.file = `${this.id}.json`; this.backing = storage.readJSON(this.file, true) || {}; this.defaults = this.constructor.defaults; Object.keys(this.defaults).forEach(k => this.bless(k)); From 98bb9c28b3aac54a7a65e2eefd28dccba5a8dbef Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 12:02:15 -0800 Subject: [PATCH 05/18] Add files via upload --- apps/pooqroman/app.png | Bin 0 -> 403 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/pooqroman/app.png diff --git a/apps/pooqroman/app.png b/apps/pooqroman/app.png new file mode 100644 index 0000000000000000000000000000000000000000..6edeebbd6e2598d0cf11410655545031ac18ddd0 GIT binary patch literal 403 zcmV;E0c`$>P)Px$O-V#SRA@u(nc)t@AP9ul_kZXuCNm)nyrV50ChVs&^y2`HJzkstj_>~8ierrH zu{%-cx+|Y!eP|~Q=7v{ecR;~SiJ(GTZ6F~mpy69WSg^x}8ApnS6_^ga4WjudSOaLO zWqQaW_R@?^6{M)AL^y?*A}(74OdGXYE>o9KqOijwyi)$R3kqBq5!=pK|=e=xC_yX zFF Date: Thu, 2 Dec 2021 12:16:53 -0800 Subject: [PATCH 06/18] Add files via upload Upload the right file this time :-}. --- apps/pooqroman/app.png | Bin 403 -> 3969 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/pooqroman/app.png b/apps/pooqroman/app.png index 6edeebbd6e2598d0cf11410655545031ac18ddd0..bd27186e04c7051f73cdf3c4e1e7afb2bcfde97d 100644 GIT binary patch literal 3969 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 Date: Thu, 2 Dec 2021 12:53:18 -0800 Subject: [PATCH 07/18] Create README.md --- apps/pooqroman/README.md | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 apps/pooqroman/README.md diff --git a/apps/pooqroman/README.md b/apps/pooqroman/README.md new file mode 100644 index 000000000..7d611c833 --- /dev/null +++ b/apps/pooqroman/README.md @@ -0,0 +1,42 @@ +# pooq Roman—a classic watch face with amusing dynamicity + +This is a normal watch face for telling the time. +It is unusual in that it supports the 24 hour clock by dynamically updating the labels on the face +(so, if you enable 24 hour mode, you will get to see a hand pointing to XXIII o'clock each evening). + +The date and day of the week can also be displayed, and they choose their own spelling depending on the available screen space. It's fun! + +## Options + +Because sometimes I don't want to burn what I'm cooking and other times I'm lazy and just want to know if it's afternoon yet, +you can alter the number of hands on the display. When the watch is unlocked, slide up to add minute and second hands, or down to remove the distraction. +There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky, in case you want +the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails. + +Although we genrally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face. +You can also override the system 12/24 hour setting just for this face here, since it's, well, a rather different experience than with numeric displays. + +One other thing: there's some integration with system timers and alarms; they will show as small pips at the appropriate places +in the day around the display. When they come within an hour, the pips turn to crosses relating to the minute hand, and the minute +hand turns itself on. When timers are mere seconds away, the display changes again and the second hand activates itself, so you +can watch as your doom approaches. + +## Limitations + +Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets. +Sorry about that, but control of all the pixels was just too important to me. + +There's also no support for internationalisation at present. This irks me, but… well, talk to me about it if there's a language you'd like. + +## The future + +The design is begging for integration with host-device calendars, and proper time zone/DST support. We'll see what the future holds. + +## Feedback + +[I'd be happy to hear your feedback](https://www.github.com/stephenPspackman) if you have comments or find any bugs, or (most especially) +if you find this work interesting. + +## By + +Made by [Stephen P Spackman](https://www.github.com/stephenPspackman). From 2906e228bad106b86c176ccf8e1639f3c67dd556 Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 12:54:03 -0800 Subject: [PATCH 08/18] Update apps.json --- apps.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps.json b/apps.json index 877218135..ae86dddc7 100644 --- a/apps.json +++ b/apps.json @@ -4641,5 +4641,21 @@ {"name":"pebble.settings.js","url":"pebble.settings.js"}, {"name":"pebble.img","url":"pebble.icon.js","evaluate":true} ] + }, + { "id": "pooqroman", + "name": "pooq Roman watch face", + "shortName":"pooq Roman", + "version":"0.0.0", + "description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"pooqroman.app.js","url":"app.js"}, + {"name":"pooqroman.img","url":"app-icon.js","evaluate":true} + ] } ] From ade94521b7da3217c6210d0d5e8873012719a792 Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 12:57:25 -0800 Subject: [PATCH 09/18] Update README.md Github flubs UTF-8? Really? --- apps/pooqroman/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pooqroman/README.md b/apps/pooqroman/README.md index 7d611c833..b41a4a316 100644 --- a/apps/pooqroman/README.md +++ b/apps/pooqroman/README.md @@ -1,4 +1,4 @@ -# pooq Roman—a classic watch face with amusing dynamicity +# pooq Roman: a classic watch face with amusing dynamicity This is a normal watch face for telling the time. It is unusual in that it supports the 24 hour clock by dynamically updating the labels on the face @@ -26,7 +26,7 @@ can watch as your doom approaches. Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets. Sorry about that, but control of all the pixels was just too important to me. -There's also no support for internationalisation at present. This irks me, but… well, talk to me about it if there's a language you'd like. +There's also no support for internationalisation at present. This irks me, but... well, talk to me about it if there's a language you'd like. ## The future From 32980bf3befaf7a047cfc808df2d9cbd547813ce Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Thu, 2 Dec 2021 13:04:29 -0800 Subject: [PATCH 10/18] Update apps.json Declare pooqroman.json, our private config file. --- apps.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps.json b/apps.json index ae86dddc7..26c4303c8 100644 --- a/apps.json +++ b/apps.json @@ -4656,6 +4656,9 @@ "storage": [ {"name":"pooqroman.app.js","url":"app.js"}, {"name":"pooqroman.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"pooqroman.json"} ] } ] From c24f67b06bbb5417355398149b8e4ad12f8d0c19 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Tue, 23 Nov 2021 21:25:26 +0100 Subject: [PATCH 11/18] settings: remove Quiet Mode LCD options Updating these will be handled by the Quiet Mode Schedule app (qmsched) --- apps.json | 2 +- apps/setting/ChangeLog | 1 + apps/setting/README.md | 2 - apps/setting/settings.js | 129 ++++----------------------------------- 4 files changed, 14 insertions(+), 120 deletions(-) diff --git a/apps.json b/apps.json index 877218135..26dfd1704 100644 --- a/apps.json +++ b/apps.json @@ -119,7 +119,7 @@ { "id": "setting", "name": "Settings", - "version": "0.33", + "version": "0.34", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index faa50405f..d840654fe 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -36,3 +36,4 @@ 0.31: Remove Bangle 1 settings when running on Bangle 2 0.32: Fix 'beep' menu on Bangle.js 2 0.33: Really fix 'beep' menu on Bangle.js 2 this time +0.34: Remove Quiet Mode LCD settings: now handled by Quiet Mode Schedule app diff --git a/apps/setting/README.md b/apps/setting/README.md index 1875fc3b0..fb567030f 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -44,6 +44,4 @@ The exact effects depend on the app. In general the watch will not wake up by i - Off: Normal operation - Alarms: Stops notifications, but "alarm" apps will still work - Silent: Blocks even alarms -* **LCD Brightness**, **LCD Timeout**, **Wake on X**: - Override default settings while Quit Mode is active (either as *Alarms* or *Silent*) \ No newline at end of file diff --git a/apps/setting/settings.js b/apps/setting/settings.js index fcf651b6f..9432d0a38 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -7,17 +7,12 @@ let settings; function updateSettings() { //storage.erase('setting.json'); // - not needed, just causes extra writes if settings were the same - if (Object.keys(settings.qmOptions).length === 0) delete settings.qmOptions; storage.write('setting.json', settings); - if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in this file } function updateOptions() { updateSettings(); Bangle.setOptions(settings.options) - if (settings.quiet) { - Bangle.setOptions(settings.qmOptions) - } } function gToInternal(g) { @@ -56,18 +51,12 @@ function resetSettings() { twistMaxY: -800, twistTimeout: 1000 }, - // Quiet Mode options: - // we only set these if we want to override the default value - // qmOptions: {}, - // qmBrightness: undefined, - // qmTimeout: undefined, }; updateSettings(); } settings = storage.readJSON('setting.json', 1); if (!settings) resetSettings(); -if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in here const boolFormat = v => v ? "On" : "Off"; @@ -130,7 +119,16 @@ function showMainMenu() { } } }, - "Quiet Mode": ()=>showQuietModeMenu(), + "Quiet Mode": { + value: settings.quiet|0, + format: v => ["Off", "Alarms", "Silent"][v%3], + onchange: v => { + settings.quiet = v%3; + updateSettings(); + updateOptions(); + if ("qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); + }, + }, 'Locale': ()=>showLocaleMenu(), 'Select Clock': ()=>showClockMenu(), 'Set Time': ()=>showSetTimeMenu(), @@ -352,9 +350,7 @@ function showLCDMenu() { onchange: v => { settings.brightness = v || 1; updateSettings(); - if (!(settings.quiet && "qmBrightness" in settings)) { - Bangle.setLCDBrightness(settings.brightness); - } + Bangle.setLCDBrightness(settings.brightness); } }, 'LCD Timeout': { @@ -365,9 +361,7 @@ function showLCDMenu() { onchange: v => { settings.timeout = 0 | v; updateSettings(); - if (!(settings.quiet && "qmTimeout" in settings)) { - Bangle.setLCDTimeout(settings.timeout); - } + Bangle.setLCDTimeout(settings.timeout); } }, 'Wake on BTN1': { @@ -455,105 +449,6 @@ function showLCDMenu() { }); return E.showMenu(lcdMenu) } -function showQuietModeMenu() { - // we always keep settings.quiet and settings.qmOptions - // other qm values are deleted when not set - const modes = ["Off", "Alarms", "Silent"]; - const qmDisabledFormat = v => v ? "Off" : "-"; - const qmMenu = { - "": {"title": "Quiet Mode"}, - "< Back": () => showMainMenu(), - "Quiet Mode": { - value: settings.quiet|0, - format: v => modes[v%3], - onchange: v => { - settings.quiet = v%3; - updateSettings(); - updateOptions(); - if ("qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} - }, - }, - "LCD Brightness": { - value: settings.qmBrightness || 0, - min: 0, // 0 = use default - max: 1, - step: 0.1, - format: v => (v>0.05) ? v : "-", - onchange: v => { - if (v>0.05) { // prevent v=0.000000000000001 bugs - settings.qmBrightness = v; - } else { - delete settings.qmBrightness; - } - updateSettings(); - if (settings.qmBrightness) { // show result, even if not quiet right now - Bangle.setLCDBrightness(v); - } else { - Bangle.setLCDBrightness(settings.brightness); - } - }, - }, - "LCD Timeout": { - value: settings.qmTimeout || 0, - min: 0, // 0 = use default (no constant on for quiet mode) - max: 60, - step: 5, - format: v => v>1 ? v : "-", - onchange: v => { - if (v>1) { - settings.qmTimeout = v; - } else { - delete settings.qmTimeout; - } - updateSettings(); - if (settings.quiet && v>1) { - Bangle.setLCDTimeout(v); - } else { - Bangle.setLCDTimeout(settings.timeout); - } - }, - }, - // we disable wakeOn* events by overwriting them as false in qmOptions - // not disabled = not present in qmOptions at all - "Wake on FaceUp": { - value: "wakeOnFaceUp" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnFaceUp" in settings.qmOptions) { - delete settings.qmOptions.wakeOnFaceUp; - } else { - settings.qmOptions.wakeOnFaceUp = false; - } - updateOptions(); - }, - }, - "Wake on Touch": { - value: "wakeOnTouch" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnTouch" in settings.qmOptions) { - delete settings.qmOptions.wakeOnTouch; - } else { - settings.qmOptions.wakeOnTouch = false; - } - updateOptions(); - }, - }, - "Wake on Twist": { - value: "wakeOnTwist" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnTwist" in settings.qmOptions) { - delete settings.qmOptions.wakeOnTwist; - } else { - settings.qmOptions.wakeOnTwist = false; - } - updateOptions(); - }, - }, - }; - return E.showMenu(qmMenu); -} function showLocaleMenu() { const localemenu = { From d743b2266abf069297de59248cb366d6b04ec63c Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Tue, 23 Nov 2021 21:27:37 +0100 Subject: [PATCH 12/18] boot: remove Quiet Mode options Updating these will be handled by the Quiet Mode Schedule app (qmsched) --- apps.json | 2 +- apps/boot/ChangeLog | 1 + apps/boot/bootupdate.js | 8 +------- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps.json b/apps.json index 26dfd1704..505412e2f 100644 --- a/apps.json +++ b/apps.json @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.36", + "version": "0.37", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 98f80efd9..ffc2be495 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -40,3 +40,4 @@ 0.35: Add Bangle.appRect polyfill Don't set beep vibration up on Bangle.js 2 (built in) 0.36: Add comments to .boot0 to make debugging a bit easier +0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index d642426c2..daf311fe6 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -78,13 +78,7 @@ boot += `E.on('errorFlag', function(errorFlags) { if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`; // Apply any settings-specific stuff if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; -if (s.quiet && s.qmOptions) boot+=`Bangle.setOptions(${E.toJS(s.qmOptions)});\n`; -if (s.quiet && s.qmBrightness) { - if (s.qmBrightness!=1) boot+=`Bangle.setLCDBrightness(${s.qmBrightness});\n`; -} else { - if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; -} -if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`; +if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`; if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; // Pre-2v10 firmwares without a theme/setUI From 3595fab079eb6a91596bc994c7d744109ed5c4fc Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 26 Nov 2021 20:05:40 +0100 Subject: [PATCH 13/18] qmsched: manage LCD options Also migrates old settings file: we now store options in qmsched.json, instead of in the global setting.json. --- apps.json | 7 +- apps/qmsched/ChangeLog | 1 + apps/qmsched/README.md | 15 +- apps/qmsched/app.js | 188 ++++++++++++++++++---- apps/qmsched/boot.js | 10 +- apps/qmsched/lib.js | 31 ++-- apps/qmsched/screenshot_b1_edit.png | Bin 0 -> 3610 bytes apps/qmsched/screenshot_b1_lcd.png | Bin 0 -> 4167 bytes apps/qmsched/screenshot_b1_main.png | Bin 0 -> 4050 bytes apps/qmsched/screenshot_b2_edit.png | Bin 0 -> 2946 bytes apps/qmsched/screenshot_b2_lcd.png | Bin 0 -> 3352 bytes apps/qmsched/screenshot_b2_main.png | Bin 0 -> 3226 bytes apps/qmsched/screenshot_edit.png | Bin 3644 -> 0 bytes apps/qmsched/screenshot_main.png | Bin 3661 -> 0 bytes apps/qmsched/screenshot_widget_alarms.png | Bin 3965 -> 0 bytes apps/qmsched/screenshot_widget_silent.png | Bin 3890 -> 0 bytes apps/qmsched/widget.js | 4 +- 17 files changed, 199 insertions(+), 57 deletions(-) create mode 100644 apps/qmsched/screenshot_b1_edit.png create mode 100644 apps/qmsched/screenshot_b1_lcd.png create mode 100644 apps/qmsched/screenshot_b1_main.png create mode 100644 apps/qmsched/screenshot_b2_edit.png create mode 100644 apps/qmsched/screenshot_b2_lcd.png create mode 100644 apps/qmsched/screenshot_b2_main.png delete mode 100644 apps/qmsched/screenshot_edit.png delete mode 100644 apps/qmsched/screenshot_main.png delete mode 100644 apps/qmsched/screenshot_widget_alarms.png delete mode 100644 apps/qmsched/screenshot_widget_silent.png diff --git a/apps.json b/apps.json index 505412e2f..5c6b78482 100644 --- a/apps.json +++ b/apps.json @@ -3797,10 +3797,11 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.03", - "description": "Automatically turn Quiet Mode on or off at set times", + "version": "0.04", + "description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.", "icon": "app.png", - "screenshots": [{"url":"screenshot_edit.png"},{"url":"screenshot_main.png"},{"url":"screenshot_widget_alarms.png"},{"url":"screenshot_widget_silent.png"}], + "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, + {"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}], "tags": "tool,widget", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog index 27b5421e8..0b8d67e76 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -1,3 +1,4 @@ 0.01: First version 0.02: Add widget 0.03: Bangle.js 2 support +0.04: Move Quiet Mode LCD options from global settings to this app diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md index 033014789..535ae56e4 100644 --- a/apps/qmsched/README.md +++ b/apps/qmsched/README.md @@ -1,9 +1,14 @@ # Quiet Mode Schedule and Widget -Automatically turn Quiet Mode on or off at set times, and display a widget when enabled. +Automatically turn Quiet Mode on or off at set times, and display a widget when Quiet Mode is active. -### Edit Schedule: -![Main menu](screenshot_main.png) ![Edit Schedule menu](screenshot_edit.png) +| Bangle.js 1 | Bangle.js 2 | +|:---------------------------------------------:|:---------------------------------------------:| +| (widget: Silent mode) | (widget: Alarms mode) | +| ![Main menu](screenshot_b1_main.png) | ![Main menu](screenshot_b2_main.png) | +| ![Edit Schedule menu](screenshot_b1_edit.png) | ![Edit Schedule menu](screenshot_b2_edit.png) | +| ![LCD Options menu](screenshot_b1_lcd.png) | ![LCD Options menu](screenshot_b2_lcd.png) | -### Widget: -![Widget, quiet mode: silent](screenshot_widget_silent.png) ![Widget, quiet mode: alarms](screenshot_widget_alarms.png) +### LCD Settings: + +If set, these override the default LCD settings while Quiet Mode is active. \ No newline at end of file diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js index c6377d4ba..7be3339fb 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -2,27 +2,74 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); const modeNames = ["Off", "Alarms", "Silent"]; -let scheds = require("Storage").readJSON("qmsched.json", 1); -/*scheds = [ - { hr : 6.5, // hours + minutes/60 - mode : 1, // quiet mode (0/1/2) - } -];*/ -if (!scheds) { - // set default schedule on first load of app - scheds = [ - {"hr": 8, "mode": 0}, - {"hr": 22, "mode": 1}, - ]; - require("Storage").writeJSON("qmsched.json", scheds); + +// load global brightness setting +let bSettings = require('Storage').readJSON('setting.json',true)||{}; +let current = 0|bSettings.quiet; +delete bSettings; // we don't need any other global settings + + + + + + +/** + * Save settings to qmsched.json + */ +function save() { + require('Storage').writeJSON('qmsched.json', settings); } -if (scheds.length && scheds.some(s => "last" in s)) { - // cleanup: remove "last" values (used by old versions) - scheds = scheds.map(s => { - delete s.last; - return s; - }); - require("Storage").writeJSON("qmsched.json", scheds); +function get(key, def) { + return (key in settings) ? settings[key] : def; +} +function set(key, val) { + settings[key] = val; save(); + scheds = settings.scheds; options = settings.options; // update references +} +function unset(key) { + delete settings[key]; save(); +} + +let settings, + scheds, options; // references for convenience +/** + * Load settings file, check if we need to migrate old setting formats to new + */ +function loadSettings() { + settings = require('Storage').readJSON("qmsched.json", true) || {}; + + if (Array.isArray(settings)) { + // migrate old file (plain array of schedules, qmOptions stored in global settings file) + require("Storage").erase("qmsched.json"); // need to erase old file, or Things Break, somehow... + let bOptions = require('Storage').readJSON('setting.json',true)||{}; + settings = { + options: bOptions.qmOptions || {}, + scheds: settings, + }; + // store new format + save(); + // and clean up qmOptions from global settings file + delete bOptions.qmOptions; + require('Storage').writeJSON('setting.json',bOptions); + } + // apply defaults + settings = Object.assign({ + options: {}, // Bangle options to override during quiet mode, default = none + scheds: [ + // default schedule: + {"hr": 8, "mode": 0}, + {"hr": 22, "mode": 1}, + ], + }, settings); + scheds = settings.scheds; options = settings.options; + + if (scheds.length && scheds.some(s => "last" in s)) { + // cleanup: remove "last" values (used by older versions) + set('scheds', scheds.map(s => { + delete s.last; + return s; + })); + } } function formatTime(t) { @@ -32,29 +79,35 @@ function formatTime(t) { } function showMainMenu() { - let menu = {"": {"title": "Quiet Mode"}}; + let _m, menu = { + "": {"title": "Quiet Mode"}, + "< Exit": () => load() + }; // "Current Mode""Silent" won't fit on Bangle.js 2 - menu["Current" + ((process.env.HWVERSION===2)?"":" Mode")]= { - value: (require("Storage").readJSON("setting.json", 1) || {}).quiet|0, + menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = { + value: current, format: v => modeNames[v], onchange: function(v) { if (v<0) {v = 2;} if (v>2) {v = 0;} require("qmsched").setMode(v); + current = v; this.value = v; }, }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { - const name = modeNames[sched.mode]; - const txt = formatTime(sched.hr)+" ".repeat(14-name.length)+name; - menu[txt] = function() { - showEditMenu(idx); + menu[formatTime(sched.hr)] = { + format: () => modeNames[sched.mode], // abuse format to right-align text + onchange: function() { + _m.draw = ()=> {}; // prevent redraw of main menu over edit menu + showEditMenu(idx); + } }; }); menu["Add Schedule"] = () => showEditMenu(-1); - menu["< Back"] = () => {load();}; - return E.showMenu(menu); + menu["LCD Settings"] = () => showOptionsMenu(); + _m = E.showMenu(menu); } function showEditMenu(index) { @@ -69,6 +122,7 @@ function showEditMenu(index) { } const menu = { "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, + "< Cancel": () => showMainMenu(), "Hours": { value: hrs, onchange: function(v) { @@ -110,18 +164,88 @@ function showEditMenu(index) { } else { scheds[index] = getSched(); } - require("Storage").writeJSON("qmsched.json", scheds); + save(); showMainMenu(); }; if (!isNew) { menu["> Delete"] = function() { scheds.splice(index, 1); - require("Storage").writeJSON("qmsched.json", scheds); + save(); showMainMenu(); }; } - menu["< Cancel"] = showMainMenu; return E.showMenu(menu); } +function showOptionsMenu() { + const disabledFormat = v => v ? "Off" : "-"; + function toggle(option) { + // we disable wakeOn* events by setting them to `false` in options + // not disabled = not present in options at all + if (option in options) { + delete options[option]; + } else { + options[option] = false; + } + save(); + } + let resetTimeout; + const oMenu = { + "": {"title": "LCD Settings"}, + "< Back": () => showMainMenu(), + "LCD Brightness": { + value: get("brightness", 0), + min: 0, // 0 = use default + max: 1, + step: 0.1, + format: v => (v>0.05) ? v : "-", + onchange: v => { + if (v>0.05) { // prevent v=0.000000000000001 bugs + set("brightness", v); + Bangle.setLCDBrightness(v); // show result, even if not quiet right now + // restore brightness after half a second + if (resetTimeout) clearTimeout(resetTimeout); + resetTimeout = setTimeout(() => { + resetTimeout = undefined; + require("qmsched").setMode(current); + }, 500); + } else { + unset("brightness"); + require("qmsched").setMode(current); + } + }, + }, + "LCD Timeout": { + value: get("timeout", 0), + min: 0, // 0 = use default (no constant on for quiet mode) + max: 60, + step: 5, + format: v => v>1 ? v : "-", + onchange: v => { + if (v>1) set("timeout", v); + else unset("timeout"); + }, + }, + // we disable wakeOn* events by overwriting them as false in options + // not disabled = not present in options at all + "Wake on FaceUp": { + value: "wakeOnFaceUp" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnFaceUp");}, + }, + "Wake on Touch": { + value: "wakeOnTouch" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnTouch");}, + }, + "Wake on Twist": { + value: "wakeOnTwist" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnTwist");}, + }, + }; + return E.showMenu(oMenu); +} + +loadSettings(); showMainMenu(); diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js index 2712cab30..c3bc49b58 100644 --- a/apps/qmsched/boot.js +++ b/apps/qmsched/boot.js @@ -1,7 +1,13 @@ // apply Quiet Mode schedules (function qm() { - let scheds = require("Storage").readJSON("qmsched.json", 1) || []; - if (!scheds.length) { return;} + let bSettings = require('Storage').readJSON('setting.json',true)||{}; + const curr = 0|bSettings.quiet; + delete bSettings; + if (curr) require("qmsched").applyOptions(curr); // no need to re-apply default options + + let settings = require('Storage').readJSON('qmsched.json',true)||{}; + let scheds = settings.scheds||[]; + if (!scheds.length) {return;} const now = new Date(), hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); // current (decimal) hour scheds.sort((a, b) => a.hr-b.hr); diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js index a3d36ed34..9b307769a 100644 --- a/apps/qmsched/lib.js +++ b/apps/qmsched/lib.js @@ -1,18 +1,23 @@ +/** + * Apply LCD options for given mode + * @param {int} mode Quiet Mode + */ +exports.applyOptions = function(mode) { + const s = require("Storage").readJSON(mode ? "qmsched.json" : "setting.json", 1) || {}; + const get = (k, d) => k in s ? s[k] : d; + Bangle.setOptions(get("options", {})); + Bangle.setLCDBrightness(get("brightness", 1)); + Bangle.setLCDTimeout(get("timeout", 10)); +}; /** * Set new Quiet Mode and apply Bangle options * @param {int} mode Quiet Mode */ exports.setMode = function(mode) { - let s = require("Storage").readJSON("setting.json", 1) || {}; - s.quiet = mode; - require("Storage").writeJSON("setting.json", s); - if (s.options) Bangle.setOptions(s.options); - if (mode && s.qmOptions) Bangle.setOptions(s.qmOptions); - if (mode && s.qmBrightness) { - if (s.qmBrightness!=1) Bangle.setLCDBrightness(s.qmBrightness); - } else { - if (s.brightness && s.brightness!=1) Bangle.setLCDBrightness(s.brightness); - } - if (mode && s.qmTimeout) Bangle.setLCDTimeout(s.qmTimeout); - if (typeof (WIDGETS)!=="undefined" && "qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} -}; \ No newline at end of file + require("Storage").writeJSON("setting.json", Object.assign( + require("Storage").readJSON("setting.json", 1) || {}, + {quiet:mode} + )); + exports.applyOptions(mode); + if (WIDGETS && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); +}; diff --git a/apps/qmsched/screenshot_b1_edit.png b/apps/qmsched/screenshot_b1_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..ec82e92e6d2de47b89b0f76f18bd1fa58f7a14a6 GIT binary patch literal 3610 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 literal 0 HcmV?d00001 diff --git a/apps/qmsched/screenshot_b1_lcd.png b/apps/qmsched/screenshot_b1_lcd.png new file mode 100644 index 0000000000000000000000000000000000000000..16f9356b89928349ae9a2c4af41dad3cb0c981b2 GIT binary patch literal 4167 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 literal 0 HcmV?d00001 diff --git a/apps/qmsched/screenshot_b1_main.png b/apps/qmsched/screenshot_b1_main.png new file mode 100644 index 0000000000000000000000000000000000000000..803ca69d5607ef9ee696fe121749d49243e70467 GIT binary patch literal 4050 zcmeHK`CpP-w};2ad{9eJQ8P_VPgzcBxAe^c^k`=0R3T0wW$LL+tu%2+AoDfNAuF{B zbEtDvB+ZEu5bBh5(k$NurzFag6Plno176PkEAIV#?hkvfAJ*_)pS{0(t+jIl{k=D; z8>+)#u#G+kJ&!1*e*WG|Kw=#_n~R!GBf$T5*N94^a3&8-0bk;I)%DpAWVn8@<_Tr#v${ zFPM+T0b2Trp6qK*EX~H0U;tXKziS>3xvc7?MK467f6p|Ni24uaP|U{%OP*S61@LB) zk2XK^Abcm`m(*?*0J>vH;pHMsMLIi*KAKS|wQ%Uc9iARQ_^SUL7A}1g0!@30DuGOu zr*pdy(jUhTy_$oRAgDemf@<*(bP_sd!LJV1{otO`YcbD2THZbexfLsOb{l)OMAd1& zUfZQJ;Lj`D(F2;@I0%F9c=<{fY*!-2e_PJNZi9mlAf2PgiR~21t!rFGn*7}4_i^8w zuT%EmE*;I%G+<4!0n5^Vn~HwPAaB53I-T=ABAIZ)^aZs{UlL?#{&2d);cet5 z`mUsCd}|laE9cA_T}q2~0fa6`aoJC%>lSfw{6+kzbJud0dIgWS1u5by|6TkD zs(aNqv%~~PX@17fsF!|NuA$xN@3=S4A4W;YW&Y5zd~PHBq3;|-3gV3(+7rGRZ-S&- z{Rk;NqgFM+?*c1E*z*!jD647B5>8(^ zHS2ksJ2H&g4RnN8L!^|+jyFq5Ksm!ji1wjPvS)fr9KvFRD{k%Nq;kAT4rRTok%{F1 zxJ6Xrd+#8Wwcqs?Qoi61f$TTcqB-D)@ZOK&;3|?H06DFQ$sdk5bzjs1Fa$D3(d=#M35P>$ z)U%Ys_ebnqX4DVYVnv#uTjbP;j14)%lsp4_hVb2tsv zh+O3#%366q<#m-MWM3{^oj>G)P86so4+IVL<<)kk4>ymEFZhz7-vILZlcC{#tM~Uv z%u$|r6H z>`{iCA+MT`F#^AV$lWG?U&CveoA~q0M5DJta8^h9Y-2?^cof_v9|pvzW`;<=l4w{g zuXm|!SIEN9m;w303x3|Y%UUi&wAY(Cr;g75ZVa4PzJM%R`-5M3QirHhXsRk4&v=D9 z`q~zJ;thjG+WpZzkqbQoGe-6QrTMrr^K~!rWS4F1-Lk#@3vUM7ZxIWe4fVNiPgvk- zP?W*sNRNZv*#`kuqUkTj{tYc^TUzvPI389~IbB{Lr_X$l6|6s$I~KK%?u_Yq4|IL&<$ddPMdi8KtR3$nU3ar*go@s0 zjN~W+6(wk2G&C>L&L#Dap{Y2)2Vv;*1a?&3Sj!;bC^m zQ;K@t-dURx7=DUK2#c_gXu(#90`+$Mkdq_FK@=stdXH-)HPTn$PT%)TjHEaCc1$f}zU+96O7 zG=HEC;OVGOWN$!8zKK^1pJP@NXIOhTtscLRZ7JkC%@&_8X-ZcO)(M>1CBjkbr0*~@ zAKbD0$!O-s-rVpHVDs?UASq#e+%h1h7V;E_6tHCK^)Qm^6@uzt%NMmJf&%;vp@NVH4fy6O%C<>bfR1B&cQrx(L#Axt zVN%D{jZ|M0mRELYd|`XF?al_rs4w}*#v*;reP#QYrRJ=&YnCLl?^*pFOs?bB)p2c! zuqSP8`~a(AFUU@aw>7@|PyDifFP*+HmtdZXxmSbr9*zy)xlqiEdcxKc(jVcB^LxYA z!c$H*x0my;GZmYh1*ED$cV5ti{P1l=^xu}?f@z!jm;>OZaQM@D%V0%`YwNd$Hanbg zRvDs37R|2=d%^#HC-2j6hi3dqtkA>i#m+$b3?Z4+#y^VEz#hFII zm3BsEw=CjKV=l47Z4$_6%*=D}HnD!DnO3_0V&Uj*432q5wVYp__r+Hpr5db)YTV|z zmhwL)hhV{9Ey;hN8q>56hkC$(HSHU-)TNEIx27;^61*Ya?~-qBirb(xb_LH|8$2J? zkOKqs9yEixezB!w(C9r+INAY`T8ZpYjy%?h9$(q~?Ru$u+j?AjZ>qhm(m3-bdil> zz&4DEGwchBcG=>+=52CNu#CeDokhHD&`HBL@)g3#FuX1uR;9|le%VfIHiNTLNw$H5 z5iLMC^A6!f>RP&L-E@Dcxq$1eE~M{dQr<8|x=#qi24@d3m>-49IQI`j{7_O!QNJQi zNS}SoSgHJCNDZZ#i4b^ksR6tJy}kMz=!R*;V3Iy@yNMb(1SEx*0@gcQgNKdXMIabL z)xY(q=cTZ#YSz|DI8~Ng9mcyr&~+tb3f7 zq=ZV&9TqrUa9#1iPf1vFQHrvgJ*>Q4<(xxa?z1FDXPZ>ZQR>^U722+Jf&Z`ZjhvDAu{~ zw+y^~cl`XXyv}^fuck!vgYA#C6W3FF|70x*n5)*&1>Ub4f*)C91pNU*hHNV9zz8a& z2w98Ed%6=2#<{3ieWmyBpMu9+-fc;(yVQCV%NtJ%lO(^q_}$8gu>eP1;_-5jSf17k z;jAuvY0s?W8pYXiR0Z$?k%_2~ZjXvA81SPaUklPzRx2-}y#{p?06oica5D|`gu(Tx zudTuTkf~_XyJZR%rQ`^x*NmJnrTl-8|NUp80V*eCAtAHlb1Q|S&vVcPjFp$J7M+2u zxTY)-!1YilNl6Xk+fq2y6G*6{$=5Qrj%hxCjE^GUS|kR%8jaQidMpZUAPb#k>u>3f zy^zypr!mtTew00l`+}z3i8??%@4m&A-KI$8_shJ`K!xh3Px=JV``BRCr$Po$Ge%Dhz~^_kZZ_<+RRrDKH};Zt?WbS!WxBH2NaT*iN30$K&gv zKPj-O0#5-Cy0y7mT0&HoF1@g=k-h1sVz!rE<`2_L3 zL7El)AAmjZ3gTJm)4Bp!3~a5yFMwOBud49Y9Bk`+7GO_5d&2LTyRCY1e7XVxczO!t zK#Kx#03Rjz`F&?Ze^OwS0!;vaeLWt}=i_nw_uT{k{QvRq`jY~^6>tN5!h1NF0(}0P zAnu($+4t>2{Im-%`7&DyO6=-`FLKS%H zll2`0~_u+s~6sykL8_*0OumOZkwS91u#-b$6FL|0h}(F+XrrO zN^_+EtAGGzmD0E+6$k-XcixuF?Jb-N1NatdDahp%5Wvf&L=IUL=nC)`xn1!jwkl3r z*Ri>Oy9SSr*Muth&iI<&x%u9E^v<^=H9~_l{?UH3@eTnvBKVO2m%xwsKtyQ4CinZ= zr0s*#I^W`}XRH8DR6_|v$4g1@&hsQSq5_{bX8Fy=y*0onNRRlQImnNQO319yqjl+} zhc}*d-f{q=zWF!>*ZMPNKGXYJPW4Q`?D31Sn^wwFY**b%v02+Ri+@qTE)8()({2^l zTlK6JhwYc6VzcUS9gkwO*kYyddzt9t{CT(e%7 zA%CqEnDGydX+180jS5;$^}Q-^FS}NNEju+vnd#vIn3n2$pa5>ayJwWVI$QuJ@>ou@ zv&O0d|5socfYa)he(c!OnmWwPc(E0ys`fS(>1L0G=SamW)$C0LN)5OA{0jz!OB*l5q+M;5aR1 zX@UX*c!KC!GEM;j9H*r$O;A7pPY_*8#wn2e)_j~{S&{-H6cE58RMr~Z6o3GqU$Rbo ziTb1Th3w4SZV@&bPGj0RmPRR-oL&Xk18Hf&96~PN&lzilJ>-?+zUQm|S$i-%<*hm{4@2kMH3e5Dk=!0Q_T>$q5X_oaVJuutb z_68VDI3h1rmfnsZ^cm)J-yr!}^P=Ob~o*&^G z=%bdpkpj#QEMDahz&C7l@lf3 z$_lWBu~>nTxCc}S9&aVRwgl+(k9nhCx>i9i#xt zSy%LSib#b(niTpgk5dEm_w zEO&n^Ab>^0)(QyVtf3*R=~B5#sFcxNzpS&jK4kCm+d|ZG~dX0Z476=#_{^8PzCOjyt`XPMTxqVe2SQZ zG>C-mmVxbo8Andoyq~(Ld0sngQZ$ZN0QJA64xso-5wqjKLVXRRRMm5#qM&O1 zdXKk2_;hHbRRP2j-9nTWMhl$tELF`iw)7!L{k7OcW35Thc)tQ^F3|tKD>C!te#c+| z>`#tnVg=Bd`F0f;NtX8E?8PCntK&WeY8wNk0{XhByITde$g38^qE_43+{OXZHVWX1 z?gF?CO3Q4d_3c^+=GhJ;FcKBb*Ku9}r1g>j*Ls`UF>kP`%@#X+YBZ8nz_V@YY9Z2w z`@N-M{O+`KUdi6oZYn}<4tqzC9rwCO$o<48n-PsOD^O!AXH3rnnRB?ScVO4*PvW(5SWSu#ynNdW=8Qa0p^S%F^wTemRS)kf{NqIFn*)9pQN zrInTQFoU*0Yul@7F5UopHj!B%5LoDV)H;F#UkTxE;ukj=M=U58h-omoZB0r++r~)IUaj{qyU3p0E>aG6%fE%r%%ocU@@?@ z0_Nv|YtdS_RJK-5&M%<=e5sT5(zB_}+SWKJP*NcKTD|o(a&+ej9anm!fLDP(J+KEh z&m6DAnkNOW3S=+htU!9l=TgynYgm2wy#O!nLRg@*jAbqYy}#tCKKx$r!duA}FfC(Q zh>^{nk5b+bg*_vN`@jgIYuFK|Z}t&McHHToC5}B)c~7sa07oPpoz{!nBgrF~rAIow z?Z!RR{drM`^uS0KOSQ}a&&)4X|I|0@79omF$LA^#vI~JiXzf)k01Hr*jn6tBvJZ?T zX$APOpH^7ZQXrpn{4*37ekTs~)ojl!fIkDk)+nYt3sHmEd+b`JJzoXYDx~JmQDEBB zXy~HL3t`Q!`r|o}^;{M11NU59wxvL;0s^>ILCv44fB>E^HvQZn3)~pt1#paxG{^!sR&W6vt7j2p sf!jxD0o+GdSskQ+03IZ?mg%FwKjP6{j;z%S*#H0l07*qoM6N<$f)zx0od5s; literal 0 HcmV?d00001 diff --git a/apps/qmsched/screenshot_b2_lcd.png b/apps/qmsched/screenshot_b2_lcd.png new file mode 100644 index 0000000000000000000000000000000000000000..3f06488c39c6741575b4223b811533352dc5f046 GIT binary patch literal 3352 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 JuhBzC{|gbmTl)Y2 literal 0 HcmV?d00001 diff --git a/apps/qmsched/screenshot_b2_main.png b/apps/qmsched/screenshot_b2_main.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d22a8b8184cce576295a1376f11da88bb96f73 GIT binary patch literal 3226 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 VS$oi8ar|rmS4^yptBvq6{|2w$^<@A6 literal 0 HcmV?d00001 diff --git a/apps/qmsched/screenshot_edit.png b/apps/qmsched/screenshot_edit.png deleted file mode 100644 index 88b7fcad4ac9df533f69b14675f1dcf3d9c25b1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3644 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 diff --git a/apps/qmsched/screenshot_widget_alarms.png b/apps/qmsched/screenshot_widget_alarms.png deleted file mode 100644 index 52dbe246463d0e7d9567d37683f8ecd9535f5414..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3965 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 E52Ui~sQ>@~ diff --git a/apps/qmsched/screenshot_widget_silent.png b/apps/qmsched/screenshot_widget_silent.png deleted file mode 100644 index 38b133650f7f83ee1ca429652f9177f95af5159d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3890 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 {return;} // no alarms // alarms still on: draw alarm icon in bottom-right corner From 633cad5cd3f312bc9db84623dfe4e9b6d7199e57 Mon Sep 17 00:00:00 2001 From: stephenPspackman <93166870+stephenPspackman@users.noreply.github.com> Date: Fri, 3 Dec 2021 00:11:07 -0800 Subject: [PATCH 14/18] Update app.js Fix (hopefully) time zone issues and boundaries in the event scanner. --- apps/pooqroman/app.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js index 8da638159..0628196a1 100644 --- a/apps/pooqroman/app.js +++ b/apps/pooqroman/app.js @@ -305,6 +305,8 @@ const formatDow = new Named(4, [ 'Saturday', 'Sat.', 'Sat', 'Sa' ]); +const hceil = x => Math.ceil(x / 3600000) * 3600000; +const hfloor = x => Math.floor(x / 3600000) * 3600000; const isString = x => typeof x == 'string'; const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; const imageHeight = i => isString(i) ? i.charCodeAt(1) : i.height; @@ -343,24 +345,24 @@ const events = { return result; }, - span: function(now, width) { + span: function(now, from, to, width) { let o = now.getTimezoneOffset() * 60000; let t = now.getTime() - o; let lfence = [], rfence = []; - this.scan(now, now - width, now + width, (e, d, p) => { + this.scan(now, from, to, (e, d, p) => { if (p) { for (let j = 0; j <= e.priority; j++) { - if (e.time < (lfence[e.priority] || t)) lfence[e.priority] = e.time; + if (d < (lfence[e.priority] || t)) lfence[e.priority] = d; } } else { for (let j = 0; j <= e.priority; j++) { - if (e.time > (rfence[e.priority] || t)) rfence[e.priority] = e.time; + if (d > (rfence[e.priority] || t)) rfence[e.priority] = d; } } }); for (let j = 0; ; j += 0.5) { if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) { - return [lfence[Math.floor(j)] || t, rfence[Math.ceil(j)] || t]; + return [lfence[Math.floor(j)] || now, rfence[Math.ceil(j)] || now]; } } }, @@ -584,11 +586,9 @@ class Roman { // Hour labels and (purely aesthetic) box; clear inner face. let keyHour = d.getHours() < 12 ? 1 : 13; - let alertSpan = events.span(d, 43200000); - let l = Math.floor(alertSpan[0] / 3600000) % 24; - let h = Math.ceil(alertSpan[1] / 3600000) % 24; - if ((l - keyHour + 24) % 24 >= 12) keyHour = l; - else if ((h - keyHour + 24) % 24 >= 12) keyHour = (h + 13) % 24; + let alertSpan = events.span(d, hceil(d) - 39600000, hfloor(d) + 39600000, 39600000); + let l = alertSpan[0].getHours(), h = alertSpan[1].getHours(); + if ((l - keyHour + 24) % 24 >= 12 || (h - keyHour + 24) % 24 >= 12) keyHour = l; if (keyHour !== state.keyHour) { state.keyHour = keyHour; g.setColor(options.bg) @@ -616,11 +616,9 @@ class Roman { } // Alerts - let b = new Date(d.getTime()); - b.setHours(keyHour, 0, 0, 0); - if (b > d) b.setDate(b.getDate() - 1); let requestedRate = events.scan( - d, b, b + 43200000, (e, t, p) => this.alert(e, t, d, p) + d, hfloor(alertSpan[0] + 0), hceil(alertSpan[1] + 0) + 1, + (e, t, p) => this.alert(e, t, d, p) ); if (rate > requestedRate) rate = requestedRate; From b5fa5fb64e60e70fc104bd60c08a7ef12b2c3146 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 3 Dec 2021 08:24:12 +0000 Subject: [PATCH 15/18] tweak for lint errors - still some assignment warnings --- apps/pooqroman/app.js | 835 ++++++++++++++++++------------------ apps/pooqroman/resourcer.js | 56 +-- 2 files changed, 445 insertions(+), 446 deletions(-) diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js index 0628196a1..d25fcf1a8 100644 --- a/apps/pooqroman/app.js +++ b/apps/pooqroman/app.js @@ -51,47 +51,46 @@ class Options { // Protocol: subclasses must have static id and defaults fields. // Only fields named in the defaults will be saved. constructor() { - this.id = this.constructor.id; - this.file = `${this.id}.json`; - this.backing = storage.readJSON(this.file, true) || {}; - this.defaults = this.constructor.defaults; - Object.keys(this.defaults).forEach(k => this.bless(k)); + this.id = this.constructor.id; + this.file = `${this.id}.json`; + this.backing = storage.readJSON(this.file, true) || {}; + this.defaults = this.constructor.defaults; + Object.keys(this.defaults).forEach(k => this.bless(k)); } writeBack(delay) { - if (this.timeout) clearTimeout(this.timeout); - this.timeout = setTimeout( - () => { - this.timeout = null; - storage.writeJSON(this.file, this.backing); - }, - delay - ); + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout( + () => { + this.timeout = null; + storage.writeJSON(this.file, this.backing); + }, + delay + ); } bless(k) { - Object.defineProperty(this, k, { - get: () => this.backing[k] == null ? this.defaults[k] : this.backing[k], - set: v => { - this.backing[k] = v; - // Ten second writeback delay, since the user will roll values up and down. - this.writeBack(10000); - return v; - } - }); - } + Object.defineProperty(this, k, { + get: () => this.backing[k] == null ? this.defaults[k] : this.backing[k], + set: v => { + this.backing[k] = v; + // Ten second writeback delay, since the user will roll values up and down. + this.writeBack(10000); + } + }); + } showMenu(m) { - if (m) { - for (const k in m) if ('init' in m[k]) m[k].value = m[k].init(); - m[''].selected = -1; // Workaround for self-selection bug. - } - E.showMenu(m); + if (m) { + for (const k in m) if ('init' in m[k]) m[k].value = m[k].init(); + m[''].selected = -1; // Workaround for self-selection bug. + } + E.showMenu(m); } reset() { - this.backing = {}; - this.writeBack(0); + this.backing = {}; + this.writeBack(0); } interact() {this.showMenu(this.menu);} @@ -99,34 +98,34 @@ class Options { class RomanOptions extends Options { constructor() { - super(); - this.menu = { - '': {title: '* face options *'}, - '< Back': _ => {this.showMenu(); this.emit('done');}, - Ticks: { - init: _ => this.resolution, - min: 0, max: 3, - onchange: x => this.resolution = x, - format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x] - }, - 'Display': { - init: _ => this.o24h == null ? 0 : 1 + this.o24h, - min: 0, max: 2, - onchange: x => this.o24h = [null, 0, 1][x], - format: x => ['system', '12h', '24h'][x] - }, - 'Day of Week': { - init: _ => this.dow, - onchange: x => this.dow = x - }, - Calendar: { - init: _ => this.calendric, - min: 0, max: 2, - onchange: x => this.calendric = x, - format: x => ['none', 'day', 'date'][x] - }, - Defaults: _ => {this.reset();} - }; + super(); + this.menu = { + '': {title: '* face options *'}, + '< Back': _ => {this.showMenu(); this.emit('done');}, + Ticks: { + init: _ => this.resolution, + min: 0, max: 3, + onchange: x => this.resolution = x, + format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x] + }, + 'Display': { + init: _ => this.o24h == null ? 0 : 1 + this.o24h, + min: 0, max: 2, + onchange: x => this.o24h = [null, 0, 1][x], + format: x => ['system', '12h', '24h'][x] + }, + 'Day of Week': { + init: _ => this.dow, + onchange: x => this.dow = x + }, + Calendar: { + init: _ => this.calendric, + min: 0, max: 2, + onchange: x => this.calendric = x, + format: x => ['none', 'day', 'date'][x] + }, + Defaults: _ => {this.reset();} + }; } } @@ -196,24 +195,24 @@ class Formattable { class Fixed extends Formattable { constructor(text) { - super(); - this.text = text; + super(); + this.text = text; } squeeze() {return false;} } class Squeezable extends Formattable { constructor(named, index) { - super(); - this.named = named; - this.index = index; - this.end = index + named.forms; + super(); + this.named = named; + this.index = index; + this.end = index + named.forms; } squeeze() { - if (this.index >= this.end) return false; - this.index++; - this.w = null; - return true; + if (this.index >= this.end) return false; + this.index++; + this.w = null; + return true; } get text() {return this.named.table[this.index];} } @@ -319,82 +318,82 @@ const events = { wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms clean: function(now, l) { - let o = now.getTimezoneOffset() * 60000; - let tf = now.getTime() + l, tw = tf - o; - // Discard stale events: - while (this.wall[0].time <= tw) this.wall.shift(); - while (this.fixed[0].time <= tf) this.fixed.shift(); + let o = now.getTimezoneOffset() * 60000; + let tf = now.getTime() + l, tw = tf - o; + // Discard stale events: + while (this.wall[0].time <= tw) this.wall.shift(); + while (this.fixed[0].time <= tf) this.fixed.shift(); }, scan: function(now, from, to, f) { - result = Infinity; - let o = now.getTimezoneOffset() * 60000; - let t = now.getTime() - o; - let c, p, i, l = from - o, h = to - o; - for (i = 0; (c = this.wall[i]).time < l; i++) ; - for (; (c = this.wall[i]).time < h; i++) { - if ((p = c.time < t) ? c.past : c.future) - result = Math.min(result, f(c, new Date(c.time + o), p)); - } - l += o; h += o; t += o; - for (i = 0; (c = this.fixed[i]).time < l; i++) ; - for (; (c = this.fixed[i]).time < h; i++) { - if ((p = c.time < t) ? c.past : c.future) - result = Math.min(f(c, new Date(c.time), p)); - } - return result; + result = Infinity; + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let c, p, i, l = from - o, h = to - o; + for (i = 0; (c = this.wall[i]).time < l; i++) ; + for (; (c = this.wall[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(result, f(c, new Date(c.time + o), p)); + } + l += o; h += o; t += o; + for (i = 0; (c = this.fixed[i]).time < l; i++) ; + for (; (c = this.fixed[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(f(c, new Date(c.time), p)); + } + return result; }, span: function(now, from, to, width) { - let o = now.getTimezoneOffset() * 60000; - let t = now.getTime() - o; - let lfence = [], rfence = []; - this.scan(now, from, to, (e, d, p) => { - if (p) { - for (let j = 0; j <= e.priority; j++) { - if (d < (lfence[e.priority] || t)) lfence[e.priority] = d; - } - } else { - for (let j = 0; j <= e.priority; j++) { - if (d > (rfence[e.priority] || t)) rfence[e.priority] = d; - } - } - }); - for (let j = 0; ; j += 0.5) { - if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) { - return [lfence[Math.floor(j)] || now, rfence[Math.ceil(j)] || now]; - } - } + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let lfence = [], rfence = []; + this.scan(now, from, to, (e, d, p) => { + if (p) { + for (let j = 0; j <= e.priority; j++) { + if (d < (lfence[e.priority] || t)) lfence[e.priority] = d; + } + } else { + for (let j = 0; j <= e.priority; j++) { + if (d > (rfence[e.priority] || t)) rfence[e.priority] = d; + } + } + }); + for (let j = 0; ; j += 0.5) { + if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) { + return [lfence[Math.floor(j)] || now, rfence[Math.ceil(j)] || now]; + } + } }, insert: function(t, wall, e) { - let v = wall ? this.wall : this.fixed; - e.time = t = t - (wall ? t.getTimezoneOffset() * 60000 : 0); - v.splice(v.findIndex(x => x.time > t), 0, e); + let v = wall ? this.wall : this.fixed; + e.time = t = t - (wall ? t.getTimezoneOffset() * 60000 : 0); + v.splice(v.findIndex(x => x.time > t), 0, e); }, loadFromSystem: function(options) { - alarms.forEach(x => { - if (x.on) { - const t = new Date(); - let h = x.hr; - let m = h % 1 * 60; - let s = m % 1 * 60; - let ms = s % 1 * 1000; - t.setHours(h - h % 1, m - m % 1, s - s % 1, ms); - // There's a race condition here, but I'm not sure what we can do about it. - if (t < Date.now() || x.last === t.getDate()) t.setDate(t.getDate() + 1); - this.insert(t, true, { - priority: 0, - past: false, // System alarms seem uninteresting if past? - future: true, - precision: x.timer ? 1000 : 60000, - colour: x.timer ? options.timerFg : options.alarmFg, - event: x - }); - } - }); - return this; + alarms.forEach(x => { + if (x.on) { + const t = new Date(); + let h = x.hr; + let m = h % 1 * 60; + let s = m % 1 * 60; + let ms = s % 1 * 1000; + t.setHours(h - h % 1, m - m % 1, s - s % 1, ms); + // There's a race condition here, but I'm not sure what we can do about it. + if (t < Date.now() || x.last === t.getDate()) t.setDate(t.getDate() + 1); + this.insert(t, true, { + priority: 0, + past: false, // System alarms seem uninteresting if past? + future: true, + precision: x.timer ? 1000 : 60000, + colour: x.timer ? options.timerFg : options.alarmFg, + event: x + }); + } + }); + return this; }, }; @@ -403,82 +402,82 @@ const events = { class Sidebar { constructor(g, x, y, w, h, options) { - this.g = g; - this.options = options; - this.x = x; - this.y = this.initY = y; - this.h = h; - this.rate = Infinity; - this.doLocked = Sidebar.status(_ => Bangle.isLocked(), lockI); - this.doHRM = Sidebar.status(_ => Bangle.isHRMOn(), HRMI); - this.doGPS = Sidebar.status(_ => Bangle.isGPSOn(), GPSI, Sidebar.gpsColour(options)); + this.g = g; + this.options = options; + this.x = x; + this.y = this.initY = y; + this.h = h; + this.rate = Infinity; + this.doLocked = Sidebar.status(_ => Bangle.isLocked(), lockI); + this.doHRM = Sidebar.status(_ => Bangle.isHRMOn(), HRMI); + this.doGPS = Sidebar.status(_ => Bangle.isGPSOn(), GPSI, Sidebar.gpsColour(options)); } reset(rate) {this.y = this.initY; this.rate = rate; return this;} print(t) { - this.y += 4 + t.print( - this.g.setColor(this.options.barFg).setFontAlign(-1, 1, 1), - this.x + 3, this.y + 4 - ); - return this; + this.y += 4 + t.print( + this.g.setColor(this.options.barFg).setFontAlign(-1, 1, 1), + this.x + 3, this.y + 4 + ); + return this; } pad(n) {this.y += n; return this;} free() {return this.h - this.y;} static status(p, i, c) { - return function() { - if (p()) { - this.g.setColor(c ? c() : this.options.barFg) - .drawImage(i, this.x + 4, this.y += 4); - this.y += imageHeight(i); - } - return this; - }; + return function() { + if (p()) { + this.g.setColor(c ? c() : this.options.barFg) + .drawImage(i, this.x + 4, this.y += 4); + this.y += imageHeight(i); + } + return this; + }; } static gpsColour(o) { - const fix = Bangle.getGPSFix(); - return fix && fix.fix ? o.active : o.barFg; + const fix = Bangle.getGPSFix(); + return fix && fix.fix ? o.active : o.barFg; } doPower() { - const c = Bangle.isCharging(); - const b = E.getBattery(); - if (c || b < 50) { - let g = this.g, x = this.x, y = this.y, options = this.options; - g.setColor(options.barFg).drawImage(batteryI, x + 4, y + 4); - g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0'); - const h = 13 * (100 - b) / 100; - g.fillRect(x + 8, y + 7 + h, x + 17, y + 20); - // Espruino disallows blank leading rows in icons, for some reason. - if (c) g.setColor(options.barBg).drawImage(chargeI, x + 4, y + 8); - this.y = y + imageHeight(batteryI) + 4; - } - return this; + const c = Bangle.isCharging(); + const b = E.getBattery(); + if (c || b < 50) { + let g = this.g, x = this.x, y = this.y, options = this.options; + g.setColor(options.barFg).drawImage(batteryI, x + 4, y + 4); + g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0'); + const h = 13 * (100 - b) / 100; + g.fillRect(x + 8, y + 7 + h, x + 17, y + 20); + // Espruino disallows blank leading rows in icons, for some reason. + if (c) g.setColor(options.barBg).drawImage(chargeI, x + 4, y + 8); + this.y = y + imageHeight(batteryI) + 4; + } + return this; } doCompass() { - if (Bangle.isCompassOn()) { - const c = Bangle.getCompass(); - const a = c && this.rate <= 1000; - this.g.setColor(a ? this.options.active : this.options.barFg).drawImage( - compassI, - this.x + 4 + imageWidth(compassI) / 2, - this.y + 4 + imageHeight(compassI) / 2, - a ? {rotate: c.heading / 180 * Math.PI} : undefined - ); - this.y += 4 + imageHeight(compassI); - } - return this; + if (Bangle.isCompassOn()) { + const c = Bangle.getCompass(); + const a = c && this.rate <= 1000; + this.g.setColor(a ? this.options.active : this.options.barFg).drawImage( + compassI, + this.x + 4 + imageWidth(compassI) / 2, + this.y + 4 + imageHeight(compassI) / 2, + a ? {rotate: c.heading / 180 * Math.PI} : undefined + ); + this.y += 4 + imageHeight(compassI); + } + return this; } } class Roman { constructor(g, events) { - this.g = g; - this.state = {}; - const options = this.options = new RomanOptions(); - this.events = events.loadFromSystem(this.options); - this.timescales = [1000, [1000, 60000], 60000, 3600000]; - this.sidebar = new Sidebar(g, barX, barY, barW, barH, options); - this.hours = Roman.hand(g, 3, 0.5, 12, _ => options.hourFg); - this.minutes = Roman.hand(g, 2, 0.9, 60, _ => options.minuteFg); - this.seconds = Roman.hand(g, 1, 0.9, 60, _ => options.secondFg); + this.g = g; + this.state = {}; + const options = this.options = new RomanOptions(); + this.events = events.loadFromSystem(this.options); + this.timescales = [1000, [1000, 60000], 60000, 3600000]; + this.sidebar = new Sidebar(g, barX, barY, barW, barH, options); + this.hours = Roman.hand(g, 3, 0.5, 12, _ => options.hourFg); + this.minutes = Roman.hand(g, 2, 0.9, 60, _ => options.minuteFg); + this.seconds = Roman.hand(g, 1, 0.9, 60, _ => options.secondFg); } reset() {this.state = {}; this.g.clear(true);} @@ -488,150 +487,150 @@ class Roman { // Watch hands. These could be improved, graphically. // If we restricted them to 60 positions, we could feasibly hand-draw them? static hand(g, w, l, d, c) { - return p => { - g.setColor(c()); - p = ((12 * p / d) + 1) % 12; - let h = l * rectW / 2; - let v = l * rectH / 2; - let poly = - p <= 2 ? [faceCX + w, faceCY, faceCX - w, faceCY, - faceCX + h * (p - 1), faceCY - v, - faceCX + h * (p - 1) + 1, faceCY - v] - : p < 6 ? [faceCX + 1, faceCY + w, faceCX + 1, faceCY - w, - faceCX + h, faceCY + v / 2 * (p - 4), - faceCX + h, faceCY + v / 2 * (p - 4) + 1] - : p <= 8 ? [faceCX - w, faceCY + 1, faceCX + w, faceCY + 1, - faceCX - h * (p - 7), faceCY + v, - faceCX - h * (p - 7) - 1, faceCY + v] - : [faceCX, faceCY - w, faceCX, faceCY + w, - faceCX - h, faceCY - v / 2 * (p - 10), - faceCX - h, faceCY - v / 2 * (p - 10) - 1]; - g.fillPoly(poly); - }; + return p => { + g.setColor(c()); + p = ((12 * p / d) + 1) % 12; + let h = l * rectW / 2; + let v = l * rectH / 2; + let poly = + p <= 2 ? [faceCX + w, faceCY, faceCX - w, faceCY, + faceCX + h * (p - 1), faceCY - v, + faceCX + h * (p - 1) + 1, faceCY - v] + : p < 6 ? [faceCX + 1, faceCY + w, faceCX + 1, faceCY - w, + faceCX + h, faceCY + v / 2 * (p - 4), + faceCX + h, faceCY + v / 2 * (p - 4) + 1] + : p <= 8 ? [faceCX - w, faceCY + 1, faceCX + w, faceCY + 1, + faceCX - h * (p - 7), faceCY + v, + faceCX - h * (p - 7) - 1, faceCY + v] + : [faceCX, faceCY - w, faceCX, faceCY + w, + faceCX - h, faceCY - v / 2 * (p - 10), + faceCX - h, faceCY - v / 2 * (p - 10) - 1]; + g.fillPoly(poly); + }; } static pos(p, r) { - let h = r * rectW / 2; - let v = r * rectH / 2; - p = (p + 1) % 12; - return p <= 2 ? [faceCX + h * (p - 1), faceCY - v] - : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)] - : p <= 8 ? [faceCX - h * (p - 7), faceCY + v] - : [faceCX - h, faceCY - v / 2 * (p - 10)]; + let h = r * rectW / 2; + let v = r * rectH / 2; + p = (p + 1) % 12; + return p <= 2 ? [faceCX + h * (p - 1), faceCY - v] + : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)] + : p <= 8 ? [faceCX - h * (p - 7), faceCY + v] + : [faceCX - h, faceCY - v / 2 * (p - 10)]; } alert(e, date, now, past) { - const g = this.g; - g.setColor(e.colour); - const dt = date - now; - if (e.precision < 60000 && dt >= 0 && e.future && dt <= 59000) { // Seconds away - const p = Roman.pos(date.getSeconds() / 5, 0.95); - g.drawLine(faceCX, faceCY, p[0], p[1]); - return 1000; - } else if (e.precision < 3600000 && dt >= 0 && e.future && dt <= 3540000) { // Minutes away - const p = Roman.pos(date.getMinutes() / 5 + date.getSeconds() / 300, 0.8); - g.drawLine(p[0] - 5, p[1], p[0] + 5, p[1]); - g.drawLine(p[0], p[1] - 5, p[0], p[1] + 5); - return dt < 119000 ? 1000 : 60000; // Turn on second hand two minutes up. - } else if (e.precision < 43200000 && dt >= 0 ? e.future : e.past) { // Hours away - const p = Roman.pos(date.getHours() + date.getMinutes() / 60, 0.6); - const poly = [p[0] - 4, p[1], p[0], p[1] - 4, p[0] + 4, p[1], p[0], p[1] + 4]; - if (date >= now) g.fillPoly(poly); - else g.drawPoly(poly, true); - return 3600000; - } - return Infinity; + const g = this.g; + g.setColor(e.colour); + const dt = date - now; + if (e.precision < 60000 && dt >= 0 && e.future && dt <= 59000) { // Seconds away + const p = Roman.pos(date.getSeconds() / 5, 0.95); + g.drawLine(faceCX, faceCY, p[0], p[1]); + return 1000; + } else if (e.precision < 3600000 && dt >= 0 && e.future && dt <= 3540000) { // Minutes away + const p = Roman.pos(date.getMinutes() / 5 + date.getSeconds() / 300, 0.8); + g.drawLine(p[0] - 5, p[1], p[0] + 5, p[1]); + g.drawLine(p[0], p[1] - 5, p[0], p[1] + 5); + return dt < 119000 ? 1000 : 60000; // Turn on second hand two minutes up. + } else if (e.precision < 43200000 && dt >= 0 ? e.future : e.past) { // Hours away + const p = Roman.pos(date.getHours() + date.getMinutes() / 60, 0.6); + const poly = [p[0] - 4, p[1], p[0], p[1] - 4, p[0] + 4, p[1], p[0], p[1] + 4]; + if (date >= now) g.fillPoly(poly); + else g.drawPoly(poly, true); + return 3600000; + } + return Infinity; } render(d, rate) { - const g = this.g; - const state = this.state; - const options = this.options; - const events = this.events; - events.clean(d, -39600000); // 11h + const g = this.g; + const state = this.state; + const options = this.options; + const events = this.events; + events.clean(d, -39600000); // 11h - // Sidebar: icons and date - if (d.getDate() !== state.date || !state.iconsOk) { - const sidebar = this.sidebar; - state.date = d.getDate(); - state.iconsOk = true; - g.setColor(options.barBg).fillRect(barX, barY, barX + barW, barY + barH); + // Sidebar: icons and date + if (d.getDate() !== state.date || !state.iconsOk) { + const sidebar = this.sidebar; + state.date = d.getDate(); + state.iconsOk = true; + g.setColor(options.barBg).fillRect(barX, barY, barX + barW, barY + barH); - sidebar.reset(rate).doLocked().doPower().doGPS().doHRM().doCompass(); - g.setFontCustom.apply(g, fontF); - let formatters = []; - let month, dom, dow; - if (options.calendric > 1) { - formatters.push(month = formatMonth.on(d.getMonth())); - } - if (options.calendric > 0) { - formatters.push(dom = formatDom.on(d.getDate())); - } - if (options.dow) { - formatters.push(dow = formatDow.on(d.getDay())); - } - // Obnoxiously inefficient iterative method :( - let ava = sidebar.free() - 3, use, i = 0, j = 0; - while ((use = formatters.reduce((l, f) => l + f.width(g) + 4, 0)) > ava && - j < formatters.length) - for (j = 0; - !formatters[i++ % formatters.length].squeeze() && - j < formatters.length; - j++) ; - if (dow) sidebar.print(dow); - sidebar.pad(ava - use); - if (month) sidebar.print(month); - if (dom) sidebar.print(dom); - } + sidebar.reset(rate).doLocked().doPower().doGPS().doHRM().doCompass(); + g.setFontCustom.apply(g, fontF); + let formatters = []; + let month, dom, dow; + if (options.calendric > 1) { + formatters.push(month = formatMonth.on(d.getMonth())); + } + if (options.calendric > 0) { + formatters.push(dom = formatDom.on(d.getDate())); + } + if (options.dow) { + formatters.push(dow = formatDow.on(d.getDay())); + } + // Obnoxiously inefficient iterative method :( + let ava = sidebar.free() - 3, use, i = 0, j = 0; + while ((use = formatters.reduce((l, f) => l + f.width(g) + 4, 0)) > ava && + j < formatters.length) + for (j = 0; + !formatters[i++ % formatters.length].squeeze() && + j < formatters.length; + j++) ; + if (dow) sidebar.print(dow); + sidebar.pad(ava - use); + if (month) sidebar.print(month); + if (dom) sidebar.print(dom); + } - // Hour labels and (purely aesthetic) box; clear inner face. - let keyHour = d.getHours() < 12 ? 1 : 13; - let alertSpan = events.span(d, hceil(d) - 39600000, hfloor(d) + 39600000, 39600000); - let l = alertSpan[0].getHours(), h = alertSpan[1].getHours(); - if ((l - keyHour + 24) % 24 >= 12 || (h - keyHour + 24) % 24 >= 12) keyHour = l; - if (keyHour !== state.keyHour) { - state.keyHour = keyHour; - g.setColor(options.bg) - .fillRect(faceX, faceY, faceX + faceW, faceY + faceH) - .setFontCustom.apply(g, romanPartsF) - .setFontAlign(0, 1) - .setColor(options.fg); - // In order to deal with timezone changes more logic will be required, - // since the labels may be in unusual locations (even offset when - // a non-integral zone is involved). The value of keyHour can be - // anything in [hr-12, hr] mod 24. - for (let h = keyHour; h < keyHour + 12; h++) { - g.drawString( - numeral(h % 24, options), - faceX + layout[h % 12 * 2], - faceY + layout[h % 12 * 2 + 1] - ); - } - g.setColor(options.rectFg) - .drawRect(rectX, rectY, rectX + rectW - 1, rectY + rectH - 1); - } else { - g.setColor(options.bg) - .fillRect(rectX + 1, rectY + 1, rectX + rectW - 2, rectY + rectH - 2) - .setColor(options.fg); - } + // Hour labels and (purely aesthetic) box; clear inner face. + let keyHour = d.getHours() < 12 ? 1 : 13; + let alertSpan = events.span(d, hceil(d) - 39600000, hfloor(d) + 39600000, 39600000); + let l = alertSpan[0].getHours(), h = alertSpan[1].getHours(); + if ((l - keyHour + 24) % 24 >= 12 || (h - keyHour + 24) % 24 >= 12) keyHour = l; + if (keyHour !== state.keyHour) { + state.keyHour = keyHour; + g.setColor(options.bg) + .fillRect(faceX, faceY, faceX + faceW, faceY + faceH) + .setFontCustom.apply(g, romanPartsF) + .setFontAlign(0, 1) + .setColor(options.fg); + // In order to deal with timezone changes more logic will be required, + // since the labels may be in unusual locations (even offset when + // a non-integral zone is involved). The value of keyHour can be + // anything in [hr-12, hr] mod 24. + for (let h = keyHour; h < keyHour + 12; h++) { + g.drawString( + numeral(h % 24, options), + faceX + layout[h % 12 * 2], + faceY + layout[h % 12 * 2 + 1] + ); + } + g.setColor(options.rectFg) + .drawRect(rectX, rectY, rectX + rectW - 1, rectY + rectH - 1); + } else { + g.setColor(options.bg) + .fillRect(rectX + 1, rectY + 1, rectX + rectW - 2, rectY + rectH - 2) + .setColor(options.fg); + } - // Alerts - let requestedRate = events.scan( - d, hfloor(alertSpan[0] + 0), hceil(alertSpan[1] + 0) + 1, - (e, t, p) => this.alert(e, t, d, p) - ); - if (rate > requestedRate) rate = requestedRate; - - // Hands - // Here we are using incremental hands for hours and minutes. - // If we quantised, we could use hand-crafted bitmaps, though. - this.hours(d.getHours() + d.getMinutes() / 60); - if (rate < 3600000) { - this.minutes(d.getMinutes() + d.getSeconds() / 60); - } - if (rate < 60000) this.seconds(d.getSeconds()); - g.setColor(options.hubFg).fillCircle(faceCX, faceCY, 3); - return requestedRate; + // Alerts + let requestedRate = events.scan( + d, hfloor(alertSpan[0] + 0), hceil(alertSpan[1] + 0) + 1, + (e, t, p) => this.alert(e, t, d, p) + ); + if (rate > requestedRate) rate = requestedRate; + + // Hands + // Here we are using incremental hands for hours and minutes. + // If we quantised, we could use hand-crafted bitmaps, though. + this.hours(d.getHours() + d.getMinutes() / 60); + if (rate < 3600000) { + this.minutes(d.getMinutes() + d.getSeconds() / 60); + } + if (rate < 60000) this.seconds(d.getSeconds()); + g.setColor(options.hubFg).fillCircle(faceCX, faceCY, 3); + return requestedRate; } } @@ -640,119 +639,119 @@ class Roman { class Clock { constructor(face) { - this.face = face; - this.timescales = face.timescales; - this.options = face.options; - this.rates = {}; + this.face = face; + this.timescales = face.timescales; + this.options = face.options; + this.rates = {}; - this.options.on('done', () => this.start()); - - this.listeners = { - lcdPower: on => on ? this.active() : this.inactive(), - charging: () => {face.doIcons('charging'); this.active();}, - lock: () => {face.doIcons('locked'); this.active();}, - faceUp: up => {this.conservative = !up; this.active();}, - drag: e => { - if (this.t0) { - if (e.b) { - e.x > this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x); - e.y > this.yN && (this.yN = e.y) || e.y > this.yX && (this.xY = e.y); - } else if (this.xX - this.xN < 20) { - if (e.y - this.e0.y < -50) { - this.options.resolution > 0 && this.options.resolution--; - this.rates.clock = this.timescales[this.options.resolution]; - this.active(); - } else if (e.y - this.e0.y > 50) { - this.options.resolution < this.timescales.length - 1 && - this.options.resolution++; - this.rates.clock = this.timescales[this.options.resolution]; - this.active(); - } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) { - this.stop(); - this.options.interact(); - } - this.t0 = null; - } - } else if (e.b) { - this.t0 = Date.now(); this.e0 = e; - this.xN = this.xX = e.x; this.yN = this.yX = e.y; - } - } - }; + this.options.on('done', () => this.start()); + + this.listeners = { + lcdPower: on => on ? this.active() : this.inactive(), + charging: () => {face.doIcons('charging'); this.active();}, + lock: () => {face.doIcons('locked'); this.active();}, + faceUp: up => {this.conservative = !up; this.active();}, + drag: e => { + if (this.t0) { + if (e.b) { + e.x > this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x); + e.y > this.yN && (this.yN = e.y) || e.y > this.yX && (this.xY = e.y); + } else if (this.xX - this.xN < 20) { + if (e.y - this.e0.y < -50) { + this.options.resolution > 0 && this.options.resolution--; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (e.y - this.e0.y > 50) { + this.options.resolution < this.timescales.length - 1 && + this.options.resolution++; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) { + this.stop(); + this.options.interact(); + } + this.t0 = null; + } + } else if (e.b) { + this.t0 = Date.now(); this.e0 = e; + this.xN = this.xX = e.x; this.yN = this.yX = e.y; + } + } + }; } redraw(rate) { - const now = this.updated = new Date(); - if (this.refresh) this.face.reset(); - this.refresh = false; - rate = this.face.render(now, rate); - if (rate !== this.rates.face) { - this.rates.face = rate; - this.active(); - } - return this; + const now = this.updated = new Date(); + if (this.refresh) this.face.reset(); + this.refresh = false; + rate = this.face.render(now, rate); + if (rate !== this.rates.face) { + this.rates.face = rate; + this.active(); + } + return this; } inactive() { - this.timeout && clearTimeout(this.timeout); - this.exception && clearTimeout(this.exception); - this.interval && clearInterval(this.interval); - this.timeout = this.exception = this.interval = this.rate = null; - return this; + this.timeout && clearTimeout(this.timeout); + this.exception && clearTimeout(this.exception); + this.interval && clearInterval(this.interval); + this.timeout = this.exception = this.interval = this.rate = null; + return this; } active() { - const prev = this.rate; - const now = Date.now(); - let rate = Infinity; - for (const k in this.rates) { - let r = this.rates[k]; - r === +r || (r = r[+this.conservative]) - r < rate && (rate = r); - } - const delay = rate - now % rate + 1; - this.refresh = true; - - if (rate !== prev) { - this.inactive(); - this.redraw(rate); - if (rate < 31622400000) { // A year! - this.timeout = setTimeout( - () => { - this.inactive(); - this.interval = setInterval(() => this.redraw(rate), rate); - if (delay > 1000) this.redraw(rate); - this.rate = rate; - }, delay - ); - } - } else if (rate > 1000) { - if (!this.exception) this.exception = setTimeout(() => { - this.redraw(rate); - this.exception = null; - }, this.updated + 1000 - Date.now()); - } - return this; + const prev = this.rate; + const now = Date.now(); + let rate = Infinity; + for (const k in this.rates) { + let r = this.rates[k]; + r === +r || (r = r[+this.conservative]) + r < rate && (rate = r); + } + const delay = rate - now % rate + 1; + this.refresh = true; + + if (rate !== prev) { + this.inactive(); + this.redraw(rate); + if (rate < 31622400000) { // A year! + this.timeout = setTimeout( + () => { + this.inactive(); + this.interval = setInterval(() => this.redraw(rate), rate); + if (delay > 1000) this.redraw(rate); + this.rate = rate; + }, delay + ); + } + } else if (rate > 1000) { + if (!this.exception) this.exception = setTimeout(() => { + this.redraw(rate); + this.exception = null; + }, this.updated + 1000 - Date.now()); + } + return this; } stop() { - this.inactive(); - for (const l in this.listeners) { - Bangle.removeListener(l, this.listeners[l]); - } - return this; + this.inactive(); + for (const l in this.listeners) { + Bangle.removeListener(l, this.listeners[l]); + } + return this; } start() { - this.inactive(); // Reset to known state. - this.conservative = false; - this.rates.clock = this.timescales[this.options.resolution]; - this.active(); - for (const l in this.listeners) { - Bangle.on(l, this.listeners[l]); - } - Bangle.setUI('clock'); - return this; + this.inactive(); // Reset to known state. + this.conservative = false; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + for (const l in this.listeners) { + Bangle.on(l, this.listeners[l]); + } + Bangle.setUI('clock'); + return this; } } diff --git a/apps/pooqroman/resourcer.js b/apps/pooqroman/resourcer.js index c172812c7..69365018e 100644 --- a/apps/pooqroman/resourcer.js +++ b/apps/pooqroman/resourcer.js @@ -63,27 +63,27 @@ const prepFont = (name, data) => { if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { const h = m[2] == '='; if (m[1].length > desc || h && m[1].length != desc) - throw new Error('Invalid descender height at ' + l); - if (m[2].length + m[3].length + m[4].length != body) - throw new Error('Invalid body height at ' + l); - if (m[5].length > asc || h && m[5].length != asc) - throw new Error('Invalid ascender height at ' + l); - if (c != null) { - lengths[c] = l - o; - if (width !== null && width !== lengths[c]) - throw new Error( - `Character has width ${lengths[c]} != ${width} at ${offsets[c]}` - ); - c = null - } - if (!h) { - c = m[3].charCodeAt(0); - if (c < min) min = c; - if (c > max) max = c; - o = l + 1; - offsets[c] = l; - adjustments[c] = m[1].length - } + throw new Error('Invalid descender height at ' + l); + if (m[2].length + m[3].length + m[4].length != body) + throw new Error('Invalid body height at ' + l); + if (m[5].length > asc || h && m[5].length != asc) + throw new Error('Invalid ascender height at ' + l); + if (c != null) { + lengths[c] = l - o; + if (width !== null && width !== lengths[c]) + throw new Error( + `Character has width ${lengths[c]} != ${width} at ${offsets[c]}` + ); + c = null + } + if (!h) { + c = m[3].charCodeAt(0); + if (c < min) min = c; + if (c > max) max = c; + o = l + 1; + offsets[c] = l; + adjustments[c] = m[1].length + } } }); const xoffs = Uint8Array(lines.length); @@ -92,16 +92,16 @@ const prepFont = (name, data) => { const w0 = lengths[min]; let widths = ''; for (c = min, o = 0; c <= max; c++) { - for (i = 0, j = offsets[c]; i < lengths[c]; i++) { - xoffs[j] = asc + body + adjustments[c] - 1; - ypos[j++] = o++; - } - widths += String.fromCharCode(lengths[c]); + for (i = 0, j = offsets[c]; i < lengths[c]; i++) { + xoffs[j] = asc + body + adjustments[c] - 1; + ypos[j++] = o++; + } + widths += String.fromCharCode(lengths[c]); } const raster = Graphics.createArrayBuffer(h, o, 1, {msb: true}); const writer = Graphics.createCallback( - image.width, image.height, 1, - (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col) + image.width, image.height, 1, + (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col) ); writer.drawImage(image); if (width === null) width = `dec(${enc(widths)})`; From 6baddd3b433d318d196c0f2d1c73df2572f2600e Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 3 Dec 2021 09:15:37 +0000 Subject: [PATCH 16/18] Add gbdebug app --- apps.json | 14 ++++++++++++++ apps/gbdebug/ChangeLog | 1 + apps/gbdebug/README.md | 26 ++++++++++++++++++++++++++ apps/gbdebug/app-icon.js | 1 + apps/gbdebug/app.js | 21 +++++++++++++++++++++ apps/gbdebug/app.png | Bin 0 -> 2914 bytes 6 files changed, 63 insertions(+) create mode 100644 apps/gbdebug/ChangeLog create mode 100644 apps/gbdebug/README.md create mode 100644 apps/gbdebug/app-icon.js create mode 100644 apps/gbdebug/app.js create mode 100644 apps/gbdebug/app.png diff --git a/apps.json b/apps.json index e6f013123..0a1fd3039 100644 --- a/apps.json +++ b/apps.json @@ -269,6 +269,20 @@ ], "data": [{"name":"gbridge.json"}] }, + { "id": "gbdebug", + "name": "Gadgetbridge Debug", + "shortName":"GB Debug", + "version":"0.01", + "description": "Debug info for Gadgetbridge. Run this app and when Gadgetbridge messages arrive they are displayed on-screen.", + "icon": "app.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"gbdebug.app.js","url":"app.js"}, + {"name":"gbdebug.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "mclock", "name": "Morphing Clock", diff --git a/apps/gbdebug/ChangeLog b/apps/gbdebug/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gbdebug/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gbdebug/README.md b/apps/gbdebug/README.md new file mode 100644 index 000000000..47b1525b8 --- /dev/null +++ b/apps/gbdebug/README.md @@ -0,0 +1,26 @@ +# Gadgetbridge Debug + +This is useful if your Bangle isn't responding to the Gadgetbridge +Android app properly. + +This app disables all existing Gadgetbridge handlers and then displays the +messages that come from Gadgetbridge on the screen +of the watch. It also saves the last 10 messages in a variable +called `history`. + +More info on Gadgetbridge at http://www.espruino.com/Gadgetbridge + +## Usage + +* Run the `GB Debug` app on your Bangle +* Connect your Bangle to Gadgetbridge +* Do whatever was causing you problems (eg receiving a call) +* The Gadgetbridge message should now be displayed on-screen + +If you want to get the *actual* data rather than copying it from the screen. + +* Ensure the `GB Debug` app is kept running after the above steps +* Disconnect Gadgetbridge from the Bangle +* Connect the Web IDE on your PC +* Type `show()` on the left-hand side of the IDE and the +last 10 messages from Gadgetbridge will be shown. diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js new file mode 100644 index 000000000..a701ef3a9 --- /dev/null +++ b/apps/gbdebug/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA=")) diff --git a/apps/gbdebug/app.js b/apps/gbdebug/app.js new file mode 100644 index 000000000..ee5e46999 --- /dev/null +++ b/apps/gbdebug/app.js @@ -0,0 +1,21 @@ +E.showMessage("Waiting for message"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var history = []; + +GB = function(e) { + if (history.length > 10) + history = history.slice(history.length-10); + history.push(e); + + var s = JSON.stringify(e,null,2); + + g.reset().clear(Bangle.appRect); + g.setFont("6x8").setFontAlign(-1,0); + g.drawString(s, 10, g.getHeight()/2); +}; + +function show() { + print(JSON.stringify(history,null,2)); +} diff --git a/apps/gbdebug/app.png b/apps/gbdebug/app.png new file mode 100644 index 0000000000000000000000000000000000000000..f70bce7ad2910e6f0746dca829f7a5311f0d0c8d GIT binary patch literal 2914 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 Date: Fri, 3 Dec 2021 09:20:13 +0000 Subject: [PATCH 17/18] Stop crypto polyfill being pulled in - fix http://forum.espruino.com/conversations/369197/#comment16294286 --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index cd3b4def8..23854083e 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit cd3b4def869cac4d7f18e7329e640e51b26758c8 +Subproject commit 23854083e0c3f83c649073a2d85e8079efc471d3 From a385c0992280070dbcbd91b8663600ea42d2749c Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 3 Dec 2021 12:04:40 +0000 Subject: [PATCH 18/18] openstmap .10: Improve scale factor calculation to fix scaling issues (#984) --- apps.json | 2 +- apps/openstmap/ChangeLog | 1 + apps/openstmap/custom.html | 47 +++++++++++++++++++++++++++++-------- apps/openstmap/openstmap.js | 12 +++++----- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/apps.json b/apps.json index 0a1fd3039..a312b90a3 100644 --- a/apps.json +++ b/apps.json @@ -1928,7 +1928,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.09", + "version": "0.10", "description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are", "icon": "app.png", "tags": "outdoors,gps", diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 60b9d9ae3..69c34ed4e 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -7,3 +7,4 @@ 0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering 0.08: Update for drag event refactor 0.09: Use current theme cols when drawing GPS info +0.10: Improve scale factor calculation to fix scaling issues (#984) diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 88d94ed37..eeb148f54 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -63,10 +63,17 @@ TODO: /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast - //var TILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; //var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white + var map = L.map('map').locate({setView: true, maxZoom: 16}); - var tileLayer = L.tileLayer(TILELAYER, { + // Tiles used for Bangle.js itself + var bangleTileLayer = L.tileLayer(TILELAYER, { + maxZoom: 18, + attribution: 'Map data © OpenStreetMap contributors' + }); + // Tiles used for the may the user sees (faster) + var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, { maxZoom: 18, attribution: 'Map data © OpenStreetMap contributors' }); @@ -83,7 +90,7 @@ TODO: } var mapFiles = []; - tileLayer.addTo(map); + previewTileLayer.addTo(map); function tilesLoaded(ctx, width, height) { var options = { @@ -122,16 +129,35 @@ TODO: } document.getElementById("getmap").addEventListener("click", function() { - var bounds = map.getBounds(); var zoom = map.getZoom(); - var centerlatlon = bounds.getCenter(); - var center = map.project(centerlatlon, zoom).divideBy(256); + var centerlatlon = map.getBounds().getCenter(); + var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE); var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE); - center = center.floor(); + center = center.floor(); // make sure we're in the middle of a tile + // JS version of Bangle.js's projection + function bproject(lat, lon) { + const degToRad = Math.PI / 180; // degree to radian conversion + const latMax = 85.0511287798; // clip latitude to sane values + const R = 6378137; // earth radius in m + if (lat > latMax) lat=latMax; + if (lat < -latMax) lat=-latMax; + var s = Math.sin(lat * degToRad); + return new L.Point( + (R * lon * degToRad), + (R * Math.log((1 + s) / (1 - s)) / 2) + ); + } + // Work out scale factors (how much from Bangle.project does one pixel equate to?) + var pc = map.unproject(center.multiplyBy(OSMTILESIZE), zoom); + var pd = map.unproject(center.multiplyBy(OSMTILESIZE).add({x:1,y:0}), zoom); + var bc = bproject(pc.lat, pc.lng) + var bd = bproject(pd.lat, pd.lng) + var scale = bc.distanceTo(bd); + var tileGetters = []; - // Render everything to a canvas - 512 x 512 px + // Render everything to a canvas... var canvas = document.getElementById("maptiles"); canvas.style.display=""; var ctx = canvas.getContext('2d'); @@ -150,7 +176,8 @@ TODO: resolve(); }; })); - img.src = tileLayer.getTileUrl(coords); + bangleTileLayer._tileZoom = previewTileLayer._tileZoom; + img.src = bangleTileLayer.getTileUrl(coords); })(i,j); } } @@ -163,7 +190,7 @@ TODO: imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, - scale : 10000*Math.pow(2,16-zoom), // FIXME - this is probably wrong + scale : scale, // how much of Bangle.project(latlon) does one pixel equate to? lat : centerlatlon.lat, lon : centerlatlon.lng })}); diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 554a71ca3..d995aca25 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -34,8 +34,8 @@ exports.draw = function() { var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); - var ix = (p.x-map.center.x)*4096/map.scale + (map.imgx/2) - cx; - var iy = (map.center.y-p.y)*4096/map.scale + (map.imgy/2) - cy; + var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx; + var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy; //console.log(ix,iy); var tx = 0|(ix/map.tilesize); var ty = 0|(iy/map.tilesize); @@ -57,8 +57,8 @@ exports.latLonToXY = function(lat, lon) { var cx = g.getWidth()/2; var cy = g.getHeight()/2; return { - x : (q.x-p.x)*4096/map.scale + cx, - y : cy - (q.y-p.y)*4096/map.scale + x : (q.x-p.x)/map.scale + cx, + y : cy - (q.y-p.y)/map.scale }; }; @@ -66,6 +66,6 @@ exports.latLonToXY = function(lat, lon) { exports.scroll = function(x,y) { var a = Bangle.project({lat:this.lat,lon:this.lon}); var b = Bangle.project({lat:this.lat+1,lon:this.lon+1}); - this.lon += x * this.map.scale / ((a.x-b.x) * 4096); - this.lat -= y * this.map.scale / ((a.y-b.y) * 4096); + this.lon += x * this.map.scale / (a.x-b.x); + this.lat -= y * this.map.scale / (a.y-b.y); };