diff --git a/README.md b/README.md
index 6ad899421..066ad0ad0 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ This is the best way to test...
**Note:** It's a great idea to get a local copy of the repository on your PC,
then run `bin/sanitycheck.js` - it'll run through a bunch of common issues
-that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule --init && git submodule update`.
+that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule update --init`.
Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect.
diff --git a/android.html b/android.html
index 48b41e72b..fb5d2557f 100644
--- a/android.html
+++ b/android.html
@@ -135,15 +135,17 @@
Utilities
+
+
+
+
+
-
-
-
Settings
@@ -172,6 +174,10 @@
Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this will break many apps)
+
diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog
index 6f306f61a..f1f8fb40e 100644
--- a/apps/alarm/ChangeLog
+++ b/apps/alarm/ChangeLog
@@ -44,3 +44,4 @@
0.39: Dated event repeat option
0.40: Use substring of message when it's longer than fits the designated menu entry.
0.41: Fix a menu bug affecting alarms with empty messages.
+0.42: Fix date not getting saved in event edit menu when tapping Confirm
diff --git a/apps/alarm/app.js b/apps/alarm/app.js
index f8ed0322e..d135f184e 100644
--- a/apps/alarm/app.js
+++ b/apps/alarm/app.js
@@ -190,7 +190,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
},
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
- prepareAlarmForSave(alarm, alarmIndex, time);
+ prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload();
showMainMenu();
}
diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json
index b986512bc..3c676c217 100644
--- a/apps/alarm/metadata.json
+++ b/apps/alarm/metadata.json
@@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
- "version": "0.41",
+ "version": "0.42",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm",
diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog
index 8362dd3a9..4587c0911 100644
--- a/apps/android/ChangeLog
+++ b/apps/android/ChangeLog
@@ -29,3 +29,4 @@
0.28: Navigation messages no longer launch the Maps view unless they're new
0.29: Support for http request xpath return format
0.30: Send firmware and hardware versions on connection
+ Allow alarm enable/disable
\ No newline at end of file
diff --git a/apps/android/boot.js b/apps/android/boot.js
index a47edb500..018ea7561 100644
--- a/apps/android/boot.js
+++ b/apps/android/boot.js
@@ -86,7 +86,7 @@
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
a.appid = "gbalarms";
- a.on = true;
+ a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.last = last;
diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog
index 11b2d7177..95b9dbaf1 100644
--- a/apps/astrocalc/ChangeLog
+++ b/apps/astrocalc/ChangeLog
@@ -2,3 +2,4 @@
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start
0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
0.04: Compatibility with Bangle.js 2, get location from My Location
+0.05: Enable widgets
diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js
index 6629842cf..2e732c37a 100644
--- a/apps/astrocalc/astrocalc-app.js
+++ b/apps/astrocalc/astrocalc-app.js
@@ -11,7 +11,6 @@
const SunCalc = require("suncalc"); // from modules folder
const storage = require("Storage");
-const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
function drawMoon(phase, x, y) {
const moonImgFiles = [
@@ -110,7 +109,7 @@ function drawPoints() {
}
function drawData(title, obj, startX, startY) {
- g.clear();
+ g.clearRect(Bangle.appRect);
drawTitle(title);
let xPos, yPos;
@@ -154,9 +153,7 @@ function drawMoonPositionPage(gps, title) {
drawPoints();
drawPoint(azimuthDegrees, 8, moonColor);
- let m = setWatch(() => {
- let m = moonIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
}
function drawMoonIlluminationPage(gps, title) {
@@ -174,9 +171,7 @@ function drawMoonIlluminationPage(gps, title) {
drawData(title, pageData, null, 35);
drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2);
- let m = setWatch(() => {
- let m = moonIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
}
@@ -202,9 +197,7 @@ function drawMoonTimesPage(gps, title) {
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
drawPoint(setAzimuthDegrees, 8, moonColor);
- let m = setWatch(() => {
- let m = moonIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
}
function drawSunShowPage(gps, key, date) {
@@ -233,9 +226,7 @@ function drawSunShowPage(gps, key, date) {
// Draw the suns position
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0});
- m = setWatch(() => {
- m = sunIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => sunIndexPageMenu(gps)});
return null;
}
@@ -314,7 +305,9 @@ function getCenterStringX(str) {
function init() {
let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
+ Bangle.loadWidgets();
indexPageMenu(location);
+ Bangle.drawWidgets();
}
let m;
diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json
index 653c097da..1f4abb356 100644
--- a/apps/astrocalc/metadata.json
+++ b/apps/astrocalc/metadata.json
@@ -1,7 +1,7 @@
{
"id": "astrocalc",
"name": "Astrocalc",
- "version": "0.04",
+ "version": "0.05",
"description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app",
"icon": "astrocalc.png",
"tags": "app,sun,moon,cycles,tool",
diff --git a/apps/chargerot/ChangeLog b/apps/chargerot/ChangeLog
index 5560f00bc..07029aebd 100644
--- a/apps/chargerot/ChangeLog
+++ b/apps/chargerot/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Handle missing settings (e.g. first-install)
diff --git a/apps/chargerot/boot.js b/apps/chargerot/boot.js
index 0a4361c50..2daeb3d50 100644
--- a/apps/chargerot/boot.js
+++ b/apps/chargerot/boot.js
@@ -1,5 +1,5 @@
(() => {
- const chargingRotation = 0 | require('Storage').readJSON("chargerot.settings.json").rotate;
+ const chargingRotation = 0 | (require('Storage').readJSON("chargerot.settings.json",1)||{}).rotate;
const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate;
if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear();
Bangle.on('charging', (charging) => {
diff --git a/apps/chargerot/metadata.json b/apps/chargerot/metadata.json
index 1b13403d7..8174836be 100644
--- a/apps/chargerot/metadata.json
+++ b/apps/chargerot/metadata.json
@@ -1,7 +1,7 @@
{
"id": "chargerot",
"name": "Charge LCD rotation",
- "version": "0.01",
+ "version": "0.02",
"description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.",
"icon": "icon.png",
"tags": "battery",
diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog
index e12b30692..870808eff 100644
--- a/apps/clock_info/ChangeLog
+++ b/apps/clock_info/ChangeLog
@@ -4,4 +4,5 @@
0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps
eg. when a clockinfo is selected, swipes won't affect swipe-down widgets
0.05: Reported image for battery is now transparent (2v18+)
-0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
\ No newline at end of file
+0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
+0.07: Developer tweak: clkinfo load errors are emitted
diff --git a/apps/clock_info/lib.js b/apps/clock_info/lib.js
index 9dd975f1e..e6c9eb27f 100644
--- a/apps/clock_info/lib.js
+++ b/apps/clock_info/lib.js
@@ -141,7 +141,7 @@ exports.load = function() {
if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a);
} catch(e){
- console.log("Could not load clock info "+E.toJS(fn));
+ console.log("Could not load clock info "+E.toJS(fn)+": "+e);
}
});
diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json
index ef9a3effa..993f112e7 100644
--- a/apps/clock_info/metadata.json
+++ b/apps/clock_info/metadata.json
@@ -1,7 +1,7 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
- "version":"0.06",
+ "version":"0.07",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",
diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog
index faf3d2d33..d147a623b 100644
--- a/apps/dragboard/ChangeLog
+++ b/apps/dragboard/ChangeLog
@@ -5,3 +5,5 @@
0.05: Now scrolls text when string gets longer than screen width.
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.
0.07: Settings for display colors
+0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js
index 83aae5f14..78ef11bd4 100644
--- a/apps/dragboard/lib.js
+++ b/apps/dragboard/lib.js
@@ -103,6 +103,133 @@ exports.input = function(options) {
initDraw();
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
+ let dragHandlerDB = function(event) {
+ "ram";
+ // ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ // Choose character by draging along red rectangle at bottom of screen
+ if (event.y >= ( (R.y+R.h) - 12 )) {
+ // Translate x-position to character
+ if (event.x < ABCPADDING) { abcHL = 0; }
+ else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
+ else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
+
+ // Datastream for development purposes
+ //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
+
+ // Unmark previous character and mark the current one...
+ // Handling switching between letters and numbers/punctuation
+ if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ if (abcHL != abcHLPrev) {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
+ }
+ // Print string at top of screen
+ if (event.b == 0) {
+ text = text + ABC.charAt(abcHL);
+ updateTopString();
+
+ // Autoswitching letter case
+ if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
+ }
+ // Update previous character to current one
+ abcHLPrev = abcHL;
+ typePrev = 'abc';
+ }
+
+ // 12345678901234567890
+ // Choose number or puctuation by draging on green rectangle
+ else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
+ // Translate x-position to character
+ if (event.x < NUMPADDING) { numHL = 0; }
+ else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
+ else { numHL = Math.floor((event.x-NUMPADDING)/6); }
+
+ // Datastream for development purposes
+ //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
+
+ // Unmark previous character and mark the current one...
+ // Handling switching between letters and numbers/punctuation
+ if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+
+ if (numHL != numHLPrev) {
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+ showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
+ }
+ // Print string at top of screen
+ if (event.b == 0) {
+ g.setColor(HLCOLOR);
+ // Backspace if releasing before list of numbers/punctuation
+ if (event.x < NUMPADDING) {
+ // show delete sign
+ showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
+ delSpaceLast = 1;
+ text = text.slice(0, -1);
+ updateTopString();
+ //print(text);
+ }
+ // Append space if releasing after list of numbers/punctuation
+ else if (event.x > (R.x+R.w)-NUMPADDING) {
+ //show space sign
+ showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
+ delSpaceLast = 1;
+ text = text + ' ';
+ updateTopString();
+ //print(text);
+ }
+ // Append selected number/punctuation
+ else {
+ text = text + NUMHIDDEN.charAt(numHL);
+ updateTopString();
+
+ // Autoswitching letter case
+ if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
+ }
+ }
+ // Update previous character to current one
+ numHLPrev = numHL;
+ typePrev = 'num';
+ }
+
+ // Make a space or backspace by swiping right or left on screen above green rectangle
+ else if (event.y > 20+4) {
+ if (event.b == 0) {
+ g.setColor(HLCOLOR);
+ if (event.x < (R.x+R.w)/2) {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ // show delete sign
+ showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
+ delSpaceLast = 1;
+
+ // Backspace and draw string upper right corner
+ text = text.slice(0, -1);
+ updateTopString();
+ if (text.length==0) changeCase(abcHL);
+ //print(text, 'undid');
+ }
+ else {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ //show space sign
+ showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
+ delSpaceLast = 1;
+
+ // Append space and draw string upper right corner
+ text = text + NUMHIDDEN.charAt(0);
+ updateTopString();
+ //print(text, 'made space');
+ }
+ }
+ }
+ };
+
+ let catchSwipe = ()=>{
+ E.stopEventPropagation&&E.stopEventPropagation();
+ };
+
function changeCase(abcHL) {
if (settings.uppercase) return;
g.setColor(BGCOLOR);
@@ -119,131 +246,12 @@ exports.input = function(options) {
mode: 'custom',
back: ()=>{
Bangle.setUI();
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(text);
},
- drag: function(event) {
- "ram";
- // ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Choose character by draging along red rectangle at bottom of screen
- if (event.y >= ( (R.y+R.h) - 12 )) {
- // Translate x-position to character
- if (event.x < ABCPADDING) { abcHL = 0; }
- else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
- else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
-
- // Datastream for development purposes
- //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
-
- // Unmark previous character and mark the current one...
- // Handling switching between letters and numbers/punctuation
- if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- if (abcHL != abcHLPrev) {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
- }
- // Print string at top of screen
- if (event.b == 0) {
- text = text + ABC.charAt(abcHL);
- updateTopString();
-
- // Autoswitching letter case
- if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
- }
- // Update previous character to current one
- abcHLPrev = abcHL;
- typePrev = 'abc';
- }
-
- // 12345678901234567890
- // Choose number or puctuation by draging on green rectangle
- else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
- // Translate x-position to character
- if (event.x < NUMPADDING) { numHL = 0; }
- else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
- else { numHL = Math.floor((event.x-NUMPADDING)/6); }
-
- // Datastream for development purposes
- //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
-
- // Unmark previous character and mark the current one...
- // Handling switching between letters and numbers/punctuation
- if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
-
- if (numHL != numHLPrev) {
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
- showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
- }
- // Print string at top of screen
- if (event.b == 0) {
- g.setColor(HLCOLOR);
- // Backspace if releasing before list of numbers/punctuation
- if (event.x < NUMPADDING) {
- // show delete sign
- showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
- delSpaceLast = 1;
- text = text.slice(0, -1);
- updateTopString();
- //print(text);
- }
- // Append space if releasing after list of numbers/punctuation
- else if (event.x > (R.x+R.w)-NUMPADDING) {
- //show space sign
- showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
- delSpaceLast = 1;
- text = text + ' ';
- updateTopString();
- //print(text);
- }
- // Append selected number/punctuation
- else {
- text = text + NUMHIDDEN.charAt(numHL);
- updateTopString();
-
- // Autoswitching letter case
- if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
- }
- }
- // Update previous character to current one
- numHLPrev = numHL;
- typePrev = 'num';
- }
-
- // Make a space or backspace by swiping right or left on screen above green rectangle
- else if (event.y > 20+4) {
- if (event.b == 0) {
- g.setColor(HLCOLOR);
- if (event.x < (R.x+R.w)/2) {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- // show delete sign
- showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
- delSpaceLast = 1;
-
- // Backspace and draw string upper right corner
- text = text.slice(0, -1);
- updateTopString();
- if (text.length==0) changeCase(abcHL);
- //print(text, 'undid');
- }
- else {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- //show space sign
- showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
- delSpaceLast = 1;
-
- // Append space and draw string upper right corner
- text = text + NUMHIDDEN.charAt(0);
- updateTopString();
- //print(text, 'made space');
- }
- }
- }
- }
+ drag: dragHandlerDB,
});
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
});
};
diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json
index 58de5153c..5c52d9389 100644
--- a/apps/dragboard/metadata.json
+++ b/apps/dragboard/metadata.json
@@ -1,6 +1,6 @@
{ "id": "dragboard",
"name": "Dragboard",
- "version":"0.07",
+ "version":"0.08",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog
index a228aab54..bca1ca7c4 100644
--- a/apps/draguboard/ChangeLog
+++ b/apps/draguboard/ChangeLog
@@ -1 +1,3 @@
0.01: New App based on dragboard, but with a U shaped drag area
+0.02: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js
index 258f8b02d..6c63668a9 100644
--- a/apps/draguboard/lib.js
+++ b/apps/draguboard/lib.js
@@ -104,45 +104,53 @@ exports.input = function(options) {
}
}
+ let dragHandlerUB = function(event) {
+ "ram";
+
+ // drag on middle bottom rectangle
+ if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
+ moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
+ }
+ // drag on left or right rectangle
+ else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
+ moveCharPos(event.x ( (R.y2) - 52 ))) {
+ moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
+ }
+ // Make a space or backspace by tapping right or left on screen above green rectangle
+ else if (event.y > R.y && event.b == 0) {
+ if (event.x < (R.x2)/2) {
+ showChars('<-');
+ text = text.slice(0, -1);
+ } else {
+ //show space sign
+ showChars('->');
+ text += ' ';
+ }
+ prevChar = null;
+ updateTopString();
+ }
+ };
+
+ let catchSwipe = ()=>{
+ E.stopEventPropagation&&E.stopEventPropagation();
+ };
+
return new Promise((resolve,reject) => {
// Interpret touch input
Bangle.setUI({
mode: 'custom',
back: ()=>{
Bangle.setUI();
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(text);
},
- drag: function(event) {
- "ram";
-
- // drag on middle bottom rectangle
- if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
- moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
- }
- // drag on left or right rectangle
- else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
- moveCharPos(event.x ( (R.y2) - 52 ))) {
- moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
- }
- // Make a space or backspace by tapping right or left on screen above green rectangle
- else if (event.y > R.y && event.b == 0) {
- if (event.x < (R.x2)/2) {
- showChars('<-');
- text = text.slice(0, -1);
- } else {
- //show space sign
- showChars('->');
- text += ' ';
- }
- prevChar = null;
- updateTopString();
- }
- }
+ drag: dragHandlerDB
});
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
R = Bangle.appRect;
MIDPADDING = R.x + 35;
diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json
index dc9b06254..620f39f71 100644
--- a/apps/draguboard/metadata.json
+++ b/apps/draguboard/metadata.json
@@ -1,6 +1,6 @@
{ "id": "draguboard",
"name": "DragUboard",
- "version":"0.01",
+ "version":"0.02",
"description": "A library for text input via swiping U-shaped keyboard.",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog
index 1155647ab..6c096f45b 100644
--- a/apps/dtlaunch/ChangeLog
+++ b/apps/dtlaunch/ChangeLog
@@ -28,4 +28,4 @@ immediately follows the correct theme.
0.22: Bangle 2: Change to not automatically marking the first app on a page
when moving pages. Add caching for faster startups.
0.23: Bangle 1: Fix issue with missing icons, added touch screen interactions
-
+0.24: Add buzz-on-interaction setting
diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js
index 2070f1147..a3ddd2538 100644
--- a/apps/dtlaunch/app-b2.js
+++ b/apps/dtlaunch/app-b2.js
@@ -9,7 +9,8 @@
showLaunchers: true,
direct: false,
swipeExit: false,
- timeOut: "Off"
+ timeOut: "Off",
+ interactionBuzz: false,
}, require('Storage').readJSON("dtlaunch.json", true) || {});
let s = require("Storage");
@@ -89,6 +90,13 @@
g.flip();
};
+ let buzzShort = function() {
+ if (settings.interactionBuzz) Bangle.buzz(20);
+ };
+ let buzzLong = function() {
+ if (settings.interactionBuzz) Bangle.buzz(100);
+ };
+
Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme.
Bangle.loadWidgets();
drawPage(0);
@@ -100,9 +108,11 @@
if(settings.swipeExit && dirLeftRight==1) Bangle.showClock();
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
+ buzzShort();
drawPage(page);
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
--page; if (page<0) page=maxPage;
+ buzzShort();
drawPage(page);
}
};
@@ -123,8 +133,10 @@
drawIcon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){
+ buzzShort();
drawIcon(page,selected,false);
} else {
+ buzzLong();
load(apps[page*4+i].src);
}
}
@@ -134,6 +146,7 @@
}
}
if ((i==4 || (page*4+i)>Napps) && selected>=0) {
+ buzzShort();
drawIcon(page,selected,false);
selected=-1;
}
diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json
index 4262ebc26..5e25b61fb 100644
--- a/apps/dtlaunch/metadata.json
+++ b/apps/dtlaunch/metadata.json
@@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
- "version": "0.23",
+ "version": "0.24",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",
diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js
index 24959df8c..6a50f90d4 100644
--- a/apps/dtlaunch/settings-b2.js
+++ b/apps/dtlaunch/settings-b2.js
@@ -6,7 +6,8 @@
showLaunchers: true,
direct: false,
swipeExit: false,
- timeOut: "Off"
+ timeOut: "Off",
+ interactionBuzz: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
@@ -55,6 +56,13 @@
settings.timeOut = timeOutChoices[v];
writeSettings();
}
- }
+ },
+ /*LANG*/'Interaction buzz': {
+ value: settings.interactionBuzz,
+ onchange: v => {
+ settings.interactionBuzz = v;
+ writeSettings();
+ }
+ },
});
});
diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog
index 4e68ab2c7..6581e5188 100644
--- a/apps/fastload/ChangeLog
+++ b/apps/fastload/ChangeLog
@@ -2,3 +2,5 @@
0.02: Allow redirection of loads to the launcher
0.03: Allow hiding the fastloading info screen
0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app).
+0.05: Check for changes in setting.js and force real reload if needed
+0.06: Fix caching whether an app is fastloadable
diff --git a/apps/fastload/README.md b/apps/fastload/README.md
index be4175f55..d82e13461 100644
--- a/apps/fastload/README.md
+++ b/apps/fastload/README.md
@@ -12,6 +12,7 @@ This allows fast loading of all apps with two conditions:
* If Quick Launch is installed it can be excluded from app history
* Allows to redirect all loads usually loading the clock to the launcher instead
* The "Fastloading..." screen can be switched off
+* Enable checking `setting.json` and force a complete load on changes
## App history
diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js
index c7fc2fd86..57bc8ea94 100644
--- a/apps/fastload/boot.js
+++ b/apps/fastload/boot.js
@@ -19,15 +19,24 @@ let loadingScreen = function(){
let cache = s.readJSON("fastload.cache") || {};
-let checkApp = function(n){
+const SYS_SETTINGS="setting.json";
+
+let appFastloadPossible = function(n){
+ if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || s.hash(SYS_SETTINGS) != cache[SYS_SETTINGS])){
+ cache[SYS_SETTINGS] = s.hash(SYS_SETTINGS);
+ s.writeJSON("fastload.cache", cache);
+ return false;
+ }
+
// no widgets, no problem
if (!global.WIDGETS) return true;
- let app = s.read(n);
- if (cache[n] && E.CRC32(app) == cache[n].crc)
+ let hash = s.hash(n);
+ if (cache[n] && hash == cache[n].hash)
return cache[n].fast;
+ let app = s.read(n);
cache[n] = {};
cache[n].fast = app.includes("Bangle.loadWidgets");
- cache[n].crc = E.CRC32(app);
+ cache[n].hash = hash;
s.writeJSON("fastload.cache", cache);
return cache[n].fast;
};
@@ -39,7 +48,7 @@ let slowload = function(n){
};
let fastload = function(n){
- if (!n || checkApp(n)){
+ if (!n || appFastloadPossible(n)){
// Bangle.load can call load, to prevent recursion this must be the system load
global.load = slowload;
Bangle.load(n);
diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json
index 954a7d8b4..8edd1f95b 100644
--- a/apps/fastload/metadata.json
+++ b/apps/fastload/metadata.json
@@ -1,7 +1,7 @@
{ "id": "fastload",
"name": "Fastload Utils",
"shortName" : "Fastload Utils",
- "version": "0.04",
+ "version": "0.06",
"icon": "icon.png",
"description": "Enable experimental fastloading for more apps",
"type":"bootloader",
diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js
index 66c990df1..15c135fe4 100644
--- a/apps/fastload/settings.js
+++ b/apps/fastload/settings.js
@@ -59,6 +59,13 @@
}
};
+ mainmenu['Detect settings changes'] = {
+ value: !!settings.detectSettingsChange,
+ onchange: v => {
+ writeSettings("detectSettingsChange",v);
+ }
+ };
+
return mainmenu;
}
diff --git a/apps/flashcards/ChangeLog b/apps/flashcards/ChangeLog
index 21b246bda..6f9ed7196 100644
--- a/apps/flashcards/ChangeLog
+++ b/apps/flashcards/ChangeLog
@@ -1,4 +1,5 @@
-1.0: Local cards data
-1.1: Download cards data from Trello public board
-1.2: Configuration instructions added and card layout optimized
-1.3: Font size can be changed in Settings
+1.00: Local cards data
+1.10: Download cards data from Trello public board
+1.20: Configuration instructions added and card layout optimized
+1.30: Font size can be changed in Settings
+1.31: Fix for fast-loading support
\ No newline at end of file
diff --git a/apps/flashcards/README.md b/apps/flashcards/README.md
index b053c25ac..484d1102f 100644
--- a/apps/flashcards/README.md
+++ b/apps/flashcards/README.md
@@ -10,7 +10,7 @@ Configuration:
4. Add ".json" to the end of the Trello board URL and refresh page
5. Find your list ID
6. Save list ID to the "flashcards.settings.json" file on your watch, e.g.:
-{"listId":"65942f7b27z68000996ddc00","fontSize":1,"cardWidth":9,"swipeGesture":0}
+{"listId":"65942f7b27z68000996ddc00","fontSize":1,"cardWidth":9,"swipeGesture":1}
7. Connect phone with Gadgetbridge to the watch
8. Enable "Allow Internet Access" in Gadgetbridge
9. On the watch go to Settings -> Apps -> Flash Cards -> Get from Trello
diff --git a/apps/flashcards/app.js b/apps/flashcards/app.js
index 5fa2e6de7..d2118f8cb 100644
--- a/apps/flashcards/app.js
+++ b/apps/flashcards/app.js
@@ -2,186 +2,185 @@
* Copyright 2023 Crisp Advice
* We believe in Finnish
*/
+{
+ // Modules
+ let Layout = require("Layout");
+ let locale = require("locale");
+ let storage = require("Storage");
-// Modules
-var Layout = require("Layout");
-var locale = require("locale");
-var storage = require("Storage");
+ // Global variables
+ const SWAP_SIDE_BUZZ_MILLISECONDS = 50;
+ const CARD_DATA_FILE = "flashcards.data.json";
+ const CARD_SETTINGS_FILE = "flashcards.settings.json";
+ const CARD_EMPTY = "no cards found";
+
+ let cards = [];
+ let cardIndex = 0;
+ let backSide = false;
+ let drawTimeout;
+ let fontSizes = ["15%","20%","25%"];
+ let lastDragX = 0;
+ let lastDragY = 0;
-// Global variables
-let SWAP_SIDE_BUZZ_MILLISECONDS = 50;
-let CARD_DATA_FILE = "flashcards.data.json";
-let CARD_SETTINGS_FILE = "flashcards.settings.json";
-let CARD_EMPTY = "no cards found";
-let cards = [];
-let cardIndex = 0;
-let backSide = false;
-let drawTimeout;
-let fontSizes = ["15%","20%","25%"];
-let lastDragX = 0;
-let lastDragY = 0;
+ let settings = Object.assign({
+ listId: "",
+ fontSize: 1,
+ cardWidth: 9,
+ swipeGesture: 1
+ }, storage.readJSON(CARD_SETTINGS_FILE, true) || {});
-let settings = Object.assign({
- listId: "",
- fontSize: 1,
- cardWidth: 9,
- swipeGesture: 0
-}, storage.readJSON(CARD_SETTINGS_FILE, true) || {});
-
-// Cards data
-function wordWrap(textStr, maxLength) {
- if (maxLength == undefined) {
- maxLength = settings.cardWidth;
- }
- let res = '';
- let str = textStr.trim();
- while (str.length > maxLength) {
- let found = false;
- // Inserts new line at first whitespace of the line
- for (i = maxLength - 1; i > 0; i--) {
- if (str.charAt(i)==' ') {
- res = res + [str.slice(0, i), "\n"].join('');
- str = str.slice(i + 1);
- found = true;
- break;
+ // Cards data
+ let wordWrap = function (textStr, maxLength) {
+ if (maxLength == undefined) {
+ maxLength = settings.cardWidth;
+ }
+ let res = '';
+ let str = textStr.trim();
+ while (str.length > maxLength) {
+ let found = false;
+ // Inserts new line at first whitespace of the line
+ for (i = maxLength - 1; i > 0; i--) {
+ if (str.charAt(i)==' ') {
+ res = res + [str.slice(0, i), "\n"].join('');
+ str = str.slice(i + 1);
+ found = true;
+ break;
+ }
+ }
+ // Inserts new line at MAX_LENGTH position, the word is too long to wrap
+ if (!found) {
+ res += [str.slice(0, maxLength), "\n"].join('');
+ str = str.slice(maxLength);
}
}
- // Inserts new line at MAX_LENGTH position, the word is too long to wrap
- if (!found) {
- res += [str.slice(0, maxLength), "\n"].join('');
- str = str.slice(maxLength);
- }
+ return res + str;
}
- return res + str;
-}
-function loadLocalCards() {
- var cardsJSON = "";
- if (storage.read(CARD_DATA_FILE))
+ let loadLocalCards = function() {
+ var cardsJSON = "";
+ if (storage.read(CARD_DATA_FILE))
+ {
+ cardsJSON = storage.readJSON(CARD_DATA_FILE, 1) || {};
+ }
+ refreshCards(cardsJSON,false);
+ }
+
+ let refreshCards = function(cardsJSON,showMsg)
{
- cardsJSON = storage.readJSON(CARD_DATA_FILE, 1) || {};
- }
- refreshCards(cardsJSON,false);
-}
+ cardIndex = 0;
+ backSide = false;
+ cards = [];
-function refreshCards(cardsJSON,showMsg)
-{
- cardIndex = 0;
- backSide = false;
- cards = [];
+ if (cardsJSON && cardsJSON.length) {
+ cardsJSON.forEach(card => {
+ cards.push([ wordWrap(card.name), wordWrap(card.desc) ]);
+ });
+ }
- if (cardsJSON && cardsJSON.length) {
- cardsJSON.forEach(card => {
- cards.push([ wordWrap(card.name), wordWrap(card.desc) ]);
- });
- }
-
- if (!cards.length) {
- cards.push([ wordWrap(CARD_EMPTY), wordWrap(CARD_EMPTY) ]);
- drawMessage("e: cards not found");
- } else if (showMsg) {
- drawMessage("i: cards refreshed");
- }
-}
-
-// Drawing a card
-let queueDraw = function() {
- let timeout = 60000;
- if (drawTimeout) clearTimeout(drawTimeout);
- drawTimeout = setTimeout(function() {
- drawTimeout = undefined;
- draw();
- }, timeout - (Date.now() % timeout));
-};
-
-let cardLayout = new Layout( {
- type:"v", c: [
- {type:"txt", font:"6x8:3", label:"", id:"widgets", fillx:1 },
- {type:"txt", font:fontSizes[settings.fontSize], label:"ABCDEFGHIJ KLMNOPQRST UVWXYZÅÖÄ", filly:1, fillx:1, id:"card" },
- {type:"txt", font:"6x8:2", label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }
- ]
-}, {lazy:true});
-
-function drawCard() {
- cardLayout.card.label = cards[cardIndex][backSide ? 1 : 0];
- cardLayout.clock.label = locale.time(new Date(),1);
- cardLayout.render();
-}
-
-function drawMessage(msg) {
- cardLayout.card.label = wordWrap(msg);
- cardLayout.render();
- console.log(msg);
-}
-
-function draw() {
- drawCard();
- Bangle.drawWidgets();
- queueDraw();
-}
-
-function swipeCard(forward)
-{
- if(forward) {
- cardIndex = (cardIndex + 1) % cards.length;
- }
- else if(--cardIndex < 0) {
- cardIndex = cards.length - 1;
- }
- drawCard();
-}
-
-// Handle a touch: swap card side
-function handleTouch(zone, event) {
- backSide = !backSide;
- drawCard();
- Bangle.buzz(SWAP_SIDE_BUZZ_MILLISECONDS);
-}
-
-// Handle a stroke event: cycle cards
-function handleStroke(event) {
- let first_x = event.xy[0];
- let last_x = event.xy[event.xy.length - 2];
- swipeCard((last_x - first_x) > 0);
-}
-
-// Handle a drag event: cycle cards
-function handleDrag(event) {
- let isFingerReleased = (event.b === 0);
- if(isFingerReleased) {
- let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) &&
- (lastDragX !== 0);
- if(isHorizontalDrag) {
- swipeCard(lastDragX > 0);
+ if (!cards.length) {
+ cards.push([ wordWrap(CARD_EMPTY), wordWrap(CARD_EMPTY) ]);
+ drawMessage("e: cards not found");
+ } else if (showMsg) {
+ drawMessage("i: cards refreshed");
}
}
- else {
- lastDragX = event.dx;
- lastDragY = event.dy;
+
+ // Drawing a card
+ let queueDraw = function() {
+ let timeout = 60000;
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, timeout - (Date.now() % timeout));
+ };
+
+ let cardLayout = new Layout( {
+ type:"v", c: [
+ {type:"txt", font:"6x8:3", label:"", id:"widgets", fillx:1 },
+ {type:"txt", font:fontSizes[settings.fontSize], label:"ABCDEFGHIJ KLMNOPQRST UVWXYZÅÖÄ", filly:1, fillx:1, id:"card" },
+ {type:"txt", font:"6x8:2", label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }
+ ]
+ }, {lazy:true});
+
+ let drawCard = function() {
+ cardLayout.card.label = cards[cardIndex][backSide ? 1 : 0];
+ cardLayout.clock.label = locale.time(new Date(),1);
+ cardLayout.render();
}
+
+ let drawMessage = function(msg) {
+ cardLayout.card.label = wordWrap(msg);
+ cardLayout.render();
+ console.log(msg);
+ }
+
+ let draw = function() {
+ drawCard();
+ Bangle.drawWidgets();
+ queueDraw();
+ }
+
+ let swipeCard = function(forward)
+ {
+ if(forward) {
+ cardIndex = (cardIndex + 1) % cards.length;
+ }
+ else if(--cardIndex < 0) {
+ cardIndex = cards.length - 1;
+ }
+ drawCard();
+ }
+
+ // Handle a touch: swap card side
+ let handleTouch = function(zone, event) {
+ backSide = !backSide;
+ drawCard();
+ Bangle.buzz(SWAP_SIDE_BUZZ_MILLISECONDS);
+ }
+
+ // Handle a stroke event: cycle cards
+ let handleStroke = function(event) {
+ let first_x = event.xy[0];
+ let last_x = event.xy[event.xy.length - 2];
+ swipeCard((last_x - first_x) > 0);
+ }
+
+ // Handle a drag event: cycle cards
+ let handleDrag = function(event) {
+ let isFingerReleased = (event.b === 0);
+ if(isFingerReleased) {
+ let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) &&
+ (lastDragX !== 0);
+ if(isHorizontalDrag) {
+ swipeCard(lastDragX > 0);
+ }
+ }
+ else {
+ lastDragX = event.dx;
+ lastDragY = event.dy;
+ }
+ }
+
+ // Ensure pressing the button goes to the launcher (by making this seem like a clock?)
+ Bangle.setUI({mode:"clock", remove:function() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ Bangle.removeListener("touch", handleTouch);
+ if (settings.swipeGesture) { Bangle.removeListener("drag", handleDrag);} else { Bangle.removeListener("stroke", handleStroke); }
+ }});
+
+ // initialize
+ cardLayout.update();
+ Bangle.loadWidgets();
+ loadLocalCards();
+
+ Bangle.on("touch", handleTouch);
+ if (settings.swipeGesture) { Bangle.on("drag", handleDrag); } else { Bangle.on("stroke", handleStroke); }
+
+ // On start: display the first card
+ g.clear();
+ draw();
}
-// Ensure pressing the button goes to the launcher (by making this seem like a clock?)
-Bangle.setUI({mode:"clock"/*, remove:function() {
- // Code to enable fast load. NOTE: this doesn't work on this app because all
- // functions and vars are declared global: https://www.espruino.com/Bangle.js+Fast+Load
- if (drawTimeout) clearTimeout(drawTimeout);
- drawTimeout = undefined;
- Bangle.removeListener("touch", handleTouch);
- if (settings.swipeGesture) { Bangle.removeListener("drag", handleDrag);} else { Bangle.removeListener("stroke", handleStroke); }
-}*/});
-
-// initialize
-cardLayout.update();
-Bangle.loadWidgets();
-loadLocalCards();
-
-Bangle.on("touch", handleTouch);
-if (settings.swipeGesture) { Bangle.on("drag", handleDrag); } else { Bangle.on("stroke", handleStroke); }
-
-// On start: display the first card
-g.clear();
-draw();
-
-
diff --git a/apps/flashcards/flashcards.settings.json b/apps/flashcards/flashcards.settings.json
index 1bb8fcf22..7c8ef545b 100644
--- a/apps/flashcards/flashcards.settings.json
+++ b/apps/flashcards/flashcards.settings.json
@@ -1 +1 @@
-{"listId":"","fontSize":1,"cardWidth":9,"swipeGesture":0}
+{"listId":"","fontSize":1,"cardWidth":9,"swipeGesture":1}
diff --git a/apps/flashcards/metadata.json b/apps/flashcards/metadata.json
index 89bb72109..096e7e918 100644
--- a/apps/flashcards/metadata.json
+++ b/apps/flashcards/metadata.json
@@ -2,7 +2,7 @@
"id": "flashcards",
"name": "Flash Cards",
"shortName": "Flash Cards",
- "version": "1.3",
+ "version": "1.31",
"description": "Flash cards based on public Trello board",
"readme":"README.md",
"screenshots" : [ { "url":"screenshot.png" }],
diff --git a/apps/flashcards/settings.js b/apps/flashcards/settings.js
index a6df1db4c..cc03349b4 100644
--- a/apps/flashcards/settings.js
+++ b/apps/flashcards/settings.js
@@ -10,7 +10,7 @@
listId: "",
fontSize: 1,
cardWidth: 9,
- swipeGesture: 0
+ swipeGesture: 1
}, storage.readJSON(settingsFile, true) || {});
function writeSettings() {
@@ -24,7 +24,7 @@
"< Back" : () => back(),
/*LANG*/"Get from Trello": () => {
if (!storage.read(settingsFile)) { writeSettings();}
- E.showPrompt("Download cards?").then((v) => {
+ E.showPrompt(/*LANG*/"Download cards?").then((v) => {
let delay = 500;
if (v) {
if (Bangle.http)
diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog
index 8646ba11a..870ad0fdb 100644
--- a/apps/gipy/ChangeLog
+++ b/apps/gipy/ChangeLog
@@ -87,3 +87,14 @@
* Reduce framerate if locked
* Stroke to move around in the map
* Fix for missing paths in display
+
+0.20:
+ * Large display for instant speed
+ * Bugfix for negative coordinates
+ * Disable menu while the map is not loaded
+ * Turn screen off while idling to save battery (with setting)
+ * New setting : disable buzz on turns
+ * New setting : turn bluetooth off to save battery
+ * New setting : power screen off between points to save battery
+ * Color change for lost direction (now purple)
+ * Adaptive screen powersaving
diff --git a/apps/gipy/README.md b/apps/gipy/README.md
index 44a8b9bcd..03ca97753 100644
--- a/apps/gipy/README.md
+++ b/apps/gipy/README.md
@@ -18,8 +18,8 @@ It provides the following features :
- display the path with current position from gps
- display a local map around you, downloaded from openstreetmap
- detects and buzzes if you leave the path
-- buzzes before sharp turns
-- buzzes before waypoints
+- (optional) buzzes before sharp turns
+- (optional) buzzes before waypoints
(for example when you need to turn in https://mapstogpx.com/)
- display instant / average speed
- display distance to next point
@@ -51,8 +51,8 @@ Your path will be displayed in svg.
### Starting Gipy
At start you will have a menu for selecting your trace (if more than one).
-Choose the one you want and you will reach the splash screen where you'll wait for the gps signal.
-Once you have a signal you will reach the main screen:
+Choose the one you want and you will reach the splash screen where you'll wait for the map.
+Once the map is loaded you will reach the main screen:

@@ -83,7 +83,7 @@ On your screen you can see:
### Lost
If you stray away from path we will rescale the display to continue displaying nearby segments and
-display the direction to follow as a black segment.
+display the direction to follow as a purple segment.
Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you
are. On path it just needed to scan a few points ahead and behind.
@@ -93,13 +93,18 @@ The distance to next point displayed corresponds to the length of the black segm
### Menu
If you click the button you'll reach a menu where you can currently zoom out to see more of the map
-(with a slower refresh rate) and reverse the path direction.
+(with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on).
### Settings
Few settings for now (feel free to suggest me more) :
- lost distance : at which distance from path are you considered to be lost ?
+- buzz on turns : should the watch buzz when reaching a waypoint ?
+- disable bluetooth : turn bluetooth off completely to try to save some power.
+- brightness : how bright should screen be ? (by default 0.5, again saving power)
+- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points,
+when you touch the screen and when speed is below 13km/h.
### Caveats
@@ -107,6 +112,7 @@ It is good to use but you should know :
- the gps might take a long time to start initially (see the assisted gps update app).
- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else.
+- if you adventure in gorges the gps signal will become garbage.
- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY**
- sometimes the watch will tell you that you are lost but you are in fact on the path. It usually figures again
the real gps position after a few minutes. It usually happens when the signal is acquired very fast.
@@ -119,4 +125,8 @@ I had to go back uphill by quite a distance.
Feel free to give me feedback : is it useful for you ? what other features would you like ?
+If you want to raise issues the main repository is [https://github.com/wagnerf42/BangleApps](here) and
+the rust code doing the actual map computations is located [https://github.com/wagnerf42/gps](here).
+You can try the cutting edge version at [https://wagnerf42.github.io/BangleApps/](https://wagnerf42.github.io/BangleApps/)
+
frederic.wagner@imag.fr
diff --git a/apps/gipy/TODO b/apps/gipy/TODO
index 266a1c5c9..b2a3c7ae1 100644
--- a/apps/gipy/TODO
+++ b/apps/gipy/TODO
@@ -1,19 +1,51 @@
+*** thoughts on lcd power ***
+so, i tried experimenting with turning the lcd off in order to save power.
+
+the good news: this saves a lot. i did a 3h ride which usually depletes the battery and I still had
+around two more hours to go.
+
+now the bad news:
+
+- i had to de-activate the twist detection : you cannot raise your watch to the eyes to turn it on.
+that's because with twist detection on all road bumps turn the watch on constantly.
+- i tried manual detection like :
+
+Bangle.on('accel', function(xyz) {
+
+ if (xyz.diff > 0.4 && xyz.mag > 1 && xyz.z < -1.4) {
+ Bangle.setLCDPower(true);
+ Bangle.setLocked(false);
+ }
+
+});
+
+this works nicely when you sit on a chair with a simulated gps signal but does not work so nicely when on the bike.
+sometimes it is ok, sometimes you can try 10 times with no success.
+
+- instead i use screen touch to turn it on. that's a bother since you need two hands but well it could be worth it.
+the problem is in the delay: between 1 and 5 seconds before the screen comes back on.
+
+
+my conclusion is that:
+
+* we should not turn screen off unless we agree to have an unresponsive ui
+* we should maybe autowake near segments ends and when lost
+* we should play with backlight instead
+
+
+**************************
+
++ when you walk the direction still has a tendency to shift
+
++ put back foot only ways
++ try fiddling with jit
++ put back street names
++ put back shortest paths but with points cache this time and jit
++ how to display paths from shortest path ?
+
+
+misc:
+ use Bangle.project(latlong)
-* additional features
-- config screen
- - are we on foot (and should use compass)
-
-- we need to buzz 200m before sharp turns (or even better, 30seconds)
-(and look at more than next point)
-
-- display distance to next water/toilet ?
-- display scale (100m)
-
-- compress path ?
-
-* misc
-
-- code is becoming messy
diff --git a/apps/gipy/app.js b/apps/gipy/app.js
index 60e4bb5af..071ef8283 100644
--- a/apps/gipy/app.js
+++ b/apps/gipy/app.js
@@ -3,65 +3,63 @@ let displaying = false;
let in_menu = false;
let go_backwards = false;
let zoomed = true;
+let powersaving = true;
let status;
let interests_colors = [
- 0xffff, // Waypoints, white
- 0xf800, // Bakery, red
- 0x001f, // DrinkingWater, blue
- 0x07ff, // Toilets, cyan
- 0x07e0, // Artwork, green
+ 0xffff, // Waypoints, white
+ 0xf800, // Bakery, red
+ 0x001f, // DrinkingWater, blue
+ 0x07ff, // Toilets, cyan
+ 0x07e0, // Artwork, green
];
let Y_OFFSET = 20;
let s = require("Storage");
-var settings = Object.assign(
- {
- lost_distance: 50,
- },
- s.readJSON("gipy.json", true) || {}
+var settings = Object.assign({
+ lost_distance: 50,
+ brightness: 0.5,
+ buzz_on_turns: false,
+ disable_bluetooth: true,
+ power_lcd_off: false,
+ },
+ s.readJSON("gipy.json", true) || {}
);
-let profile_start_times = [];
+// let profile_start_times = [];
-let splashscreen = require("heatshrink").decompress(
- atob(
- "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"
- )
-);
+// function start_profiling() {
+// profile_start_times.push(getTime());
+// }
-function start_profiling() {
- profile_start_times.push(getTime());
-}
-
-function end_profiling(label) {
- let end_time = getTime();
- let elapsed = end_time - profile_start_times.pop();
- console.log("profile:", label, "took", elapsed);
-}
+// function end_profiling(label) {
+// let end_time = getTime();
+// let elapsed = end_time - profile_start_times.pop();
+// console.log("profile:", label, "took", elapsed);
+// }
// return the index of the largest element of the array which is <= x
function binary_search(array, x) {
- let start = 0,
- end = array.length;
+ let start = 0,
+ end = array.length;
- while (end - start >= 0) {
- let mid = Math.floor((start + end) / 2);
- if (array[mid] == x) {
- return mid;
- } else if (array[mid] < x) {
- if (array[mid + 1] > x) {
- return mid;
- }
- start = mid + 1;
- } else end = mid - 1;
- }
- if (array[start] > x) {
- return null;
- } else {
- return start;
- }
+ while (end - start >= 0) {
+ let mid = Math.floor((start + end) / 2);
+ if (array[mid] == x) {
+ return mid;
+ } else if (array[mid] < x) {
+ if (array[mid + 1] > x) {
+ return mid;
+ }
+ start = mid + 1;
+ } else end = mid - 1;
+ }
+ if (array[start] > x) {
+ return null;
+ } else {
+ return start;
+ }
}
// return a string containing estimated time of arrival.
@@ -69,1438 +67,1513 @@ function binary_search(array, x) {
// remaining distance in km
// hour, minutes is current time
function compute_eta(hour, minutes, approximate_speed, remaining_distance) {
- if (isNaN(approximate_speed) || approximate_speed < 0.1) {
- return "";
- }
- let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes
- let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed);
- let eta_minutes = eta_in_minutes % 60;
- let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24;
- if (eta_minutes < 10) {
- return eta_hour.toString() + ":0" + eta_minutes;
- } else {
- return eta_hour.toString() + ":" + eta_minutes;
- }
+ if (isNaN(approximate_speed) || approximate_speed < 0.1) {
+ return "";
+ }
+ let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes
+ let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed);
+ let eta_minutes = eta_in_minutes % 60;
+ let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24;
+ if (eta_minutes < 10) {
+ return eta_hour.toString() + ":0" + eta_minutes;
+ } else {
+ return eta_hour.toString() + ":" + eta_minutes;
+ }
}
class TilesOffsets {
- constructor(buffer, offset) {
- let type_size = Uint8Array(buffer, offset, 1)[0];
- offset += 1;
- this.entry_size = Uint8Array(buffer, offset, 1)[0];
- offset += 1;
- let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0];
- offset += 2;
- this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number);
- offset += 2 * non_empty_tiles_number;
- if (type_size == 24) {
- this.non_empty_tiles_ends = Uint24Array(
- buffer,
- offset,
- non_empty_tiles_number
- );
- offset += 3 * non_empty_tiles_number;
- } else if (type_size == 16) {
- this.non_empty_tiles_ends = Uint16Array(
- buffer,
- offset,
- non_empty_tiles_number
- );
- offset += 2 * non_empty_tiles_number;
- } else {
- throw "unknown size";
+ constructor(buffer, offset) {
+ let type_size = Uint8Array(buffer, offset, 1)[0];
+ offset += 1;
+ this.entry_size = Uint8Array(buffer, offset, 1)[0];
+ offset += 1;
+ let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0];
+ offset += 2;
+ this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number);
+ offset += 2 * non_empty_tiles_number;
+ if (type_size == 24) {
+ this.non_empty_tiles_ends = Uint24Array(
+ buffer,
+ offset,
+ non_empty_tiles_number
+ );
+ offset += 3 * non_empty_tiles_number;
+ } else if (type_size == 16) {
+ this.non_empty_tiles_ends = Uint16Array(
+ buffer,
+ offset,
+ non_empty_tiles_number
+ );
+ offset += 2 * non_empty_tiles_number;
+ } else {
+ throw "unknown size";
+ }
+ return [this, offset];
}
- return [this, offset];
- }
- tile_start_offset(tile_index) {
- if (tile_index <= this.non_empty_tiles[0]) {
- return 0;
- } else {
- return this.tile_end_offset(tile_index - 1);
+ tile_start_offset(tile_index) {
+ if (tile_index <= this.non_empty_tiles[0]) {
+ return 0;
+ } else {
+ return this.tile_end_offset(tile_index - 1);
+ }
}
- }
- tile_end_offset(tile_index) {
- let me_or_before = binary_search(this.non_empty_tiles, tile_index);
- if (me_or_before === null) {
- return 0;
+ tile_end_offset(tile_index) {
+ let me_or_before = binary_search(this.non_empty_tiles, tile_index);
+ if (me_or_before === null) {
+ return 0;
+ }
+ if (me_or_before >= this.non_empty_tiles_ends.length) {
+ return (
+ this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] *
+ this.entry_size
+ );
+ } else {
+ return this.non_empty_tiles_ends[me_or_before] * this.entry_size;
+ }
}
- if (me_or_before >= this.non_empty_tiles_ends.length) {
- return (
- this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] *
- this.entry_size
- );
- } else {
- return this.non_empty_tiles_ends[me_or_before] * this.entry_size;
+ end_offset() {
+ return (
+ this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] *
+ this.entry_size
+ );
}
- }
- end_offset() {
- return (
- this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] *
- this.entry_size
- );
- }
}
class Map {
- constructor(buffer, offset, filename) {
- this.points_cache = []; // don't refetch points all the time
- // header
- let color_array = Uint8Array(buffer, offset, 3);
- this.color = [
- color_array[0] / 255,
- color_array[1] / 255,
- color_array[2] / 255,
- ];
- offset += 3;
- this.first_tile = Uint32Array(buffer, offset, 2); // absolute tile id of first tile
- offset += 2 * 4;
- this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height
- offset += 2 * 4;
- this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates
- offset += 2 * 8;
- let side_array = Float64Array(buffer, offset, 1); // side of a tile
- this.side = side_array[0];
- offset += 8;
+ constructor(buffer, offset, filename) {
+ this.points_cache = []; // don't refetch points all the time
+ // header
+ let color_array = Uint8Array(buffer, offset, 3);
+ this.color = [
+ color_array[0] / 255,
+ color_array[1] / 255,
+ color_array[2] / 255,
+ ];
+ offset += 3;
+ this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile
+ offset += 2 * 4;
+ this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height
+ offset += 2 * 4;
+ this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates
+ offset += 2 * 8;
+ let side_array = Float64Array(buffer, offset, 1); // side of a tile
+ this.side = side_array[0];
+ offset += 8;
- // tiles offsets
- let res = new TilesOffsets(buffer, offset);
- this.tiles_offsets = res[0];
- offset = res[1];
+ // tiles offsets
+ let res = new TilesOffsets(buffer, offset);
+ this.tiles_offsets = res[0];
+ offset = res[1];
- // now, do binary ways
- // since the file is so big we'll go line by line
- let binary_lines = [];
- for (let y = 0; y < this.grid_size[1]; y++) {
- let first_tile_start = this.tiles_offsets.tile_start_offset(
- y * this.grid_size[0]
- );
- let last_tile_end = this.tiles_offsets.tile_start_offset(
- (y + 1) * this.grid_size[0]
- );
- let size = last_tile_end - first_tile_start;
- let string = s.read(filename, offset + first_tile_start, size);
- let array = Uint8Array(E.toArrayBuffer(string));
- binary_lines.push(array);
- }
- this.binary_lines = binary_lines;
- offset += this.tiles_offsets.end_offset();
-
- return [this, offset];
-
- // now do streets data header
- // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8));
- // let streets_header_offset = 0;
- // let full_streets_size = Uint32Array(
- // streets_header,
- // streets_header_offset,
- // 1
- // )[0];
- // streets_header_offset += 4;
- // let blocks_number = Uint16Array(
- // streets_header,
- // streets_header_offset,
- // 1
- // )[0];
- // streets_header_offset += 2;
- // let labels_string_size = Uint16Array(
- // streets_header,
- // streets_header_offset,
- // 1
- // )[0];
- // streets_header_offset += 2;
- // offset += streets_header_offset;
-
- // // continue with main streets labels
- // main_streets_labels = s.read(filename, offset, labels_string_size);
- // // this.main_streets_labels = main_streets_labels.split(/\r?\n/);
- // this.main_streets_labels = main_streets_labels.split(/\n/);
- // offset += labels_string_size;
-
- // // continue with blocks start offsets
- // this.blocks_offsets = Uint32Array(
- // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4))
- // );
- // offset += blocks_number * 4;
-
- // // continue with compressed street blocks
- // let encoded_blocks_size =
- // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4;
- // this.compressed_streets = Uint8Array(
- // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size))
- // );
- // offset += encoded_blocks_size;
- }
-
- display(
- displayed_x,
- displayed_y,
- scale_factor,
- cos_direction,
- sin_direction
- ) {
- g.setColor(this.color[0], this.color[1], this.color[2]);
- let local_x = displayed_x - this.start_coordinates[0];
- let local_y = displayed_y - this.start_coordinates[1];
- let tile_x = Math.floor(local_x / this.side);
- let tile_y = Math.floor(local_y / this.side);
-
- let limit = 1;
- if (!zoomed) {
- limit = 2;
- }
- for (let y = tile_y - limit; y <= tile_y + limit; y++) {
- if (y < 0 || y >= this.grid_size[1]) {
- continue;
- }
- for (let x = tile_x - limit; x <= tile_x + limit; x++) {
- if (x < 0 || x >= this.grid_size[0]) {
- continue;
- }
- if (
- this.tile_is_on_screen(
- x,
- y,
- local_x,
- local_y,
- scale_factor,
- cos_direction,
- sin_direction
- )
- ) {
-// let colors = [
-// [0, 0, 0],
-// [0, 0, 1],
-// [0, 1, 0],
-// [0, 1, 1],
-// [1, 0, 0],
-// [1, 0, 1],
-// [1, 1, 0],
-// [1, 1, 0.5],
-// [0.5, 0, 0.5],
-// [0, 0.5, 0.5],
-// ];
- if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) {
- this.display_thick_tile(
- x,
- y,
- local_x,
- local_y,
- scale_factor,
- cos_direction,
- sin_direction
+ // now, do binary ways
+ // since the file is so big we'll go line by line
+ let binary_lines = [];
+ for (let y = 0; y < this.grid_size[1]; y++) {
+ let first_tile_start = this.tiles_offsets.tile_start_offset(
+ y * this.grid_size[0]
);
- } else {
- this.display_tile(
- x,
- y,
- local_x,
- local_y,
- scale_factor,
- cos_direction,
- sin_direction
+ let last_tile_end = this.tiles_offsets.tile_start_offset(
+ (y + 1) * this.grid_size[0]
);
- }
+ let size = last_tile_end - first_tile_start;
+ let string = s.read(filename, offset + first_tile_start, size);
+ let array = Uint8Array(E.toArrayBuffer(string));
+ binary_lines.push(array);
}
- }
+ this.binary_lines = binary_lines;
+ offset += this.tiles_offsets.end_offset();
+
+ return [this, offset];
+
+ // now do streets data header
+ // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8));
+ // let streets_header_offset = 0;
+ // let full_streets_size = Uint32Array(
+ // streets_header,
+ // streets_header_offset,
+ // 1
+ // )[0];
+ // streets_header_offset += 4;
+ // let blocks_number = Uint16Array(
+ // streets_header,
+ // streets_header_offset,
+ // 1
+ // )[0];
+ // streets_header_offset += 2;
+ // let labels_string_size = Uint16Array(
+ // streets_header,
+ // streets_header_offset,
+ // 1
+ // )[0];
+ // streets_header_offset += 2;
+ // offset += streets_header_offset;
+
+ // // continue with main streets labels
+ // main_streets_labels = s.read(filename, offset, labels_string_size);
+ // // this.main_streets_labels = main_streets_labels.split(/\r?\n/);
+ // this.main_streets_labels = main_streets_labels.split(/\n/);
+ // offset += labels_string_size;
+
+ // // continue with blocks start offsets
+ // this.blocks_offsets = Uint32Array(
+ // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4))
+ // );
+ // offset += blocks_number * 4;
+
+ // // continue with compressed street blocks
+ // let encoded_blocks_size =
+ // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4;
+ // this.compressed_streets = Uint8Array(
+ // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size))
+ // );
+ // offset += encoded_blocks_size;
}
- }
- tile_is_on_screen(
- tile_x,
- tile_y,
- current_x,
- current_y,
- scale_factor,
- cos_direction,
- sin_direction
- ) {
- let width = g.getWidth();
- let height = g.getHeight();
- let center_x = width / 2;
- let center_y = height / 2 + Y_OFFSET;
- let side = this.side;
- let tile_center_x = (tile_x + 0.5) * side;
- let tile_center_y = (tile_y + 0.5) * side;
- let scaled_center_x = (tile_center_x - current_x) * scale_factor;
- let scaled_center_y = (tile_center_y - current_y) * scale_factor;
- let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction;
- let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction;
- let on_screen_center_x = center_x - rotated_center_x;
- let on_screen_center_y = center_y + rotated_center_y;
+ display(
+ displayed_x,
+ displayed_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ ) {
+ g.setColor(this.color[0], this.color[1], this.color[2]);
+ let local_x = displayed_x - this.start_coordinates[0];
+ let local_y = displayed_y - this.start_coordinates[1];
+ let tile_x = Math.floor(local_x / this.side);
+ let tile_y = Math.floor(local_y / this.side);
- let scaled_side = side * scale_factor * Math.sqrt(1/2);
-
- if (on_screen_center_x + scaled_side <= 0) {
- return false;
+ let limit = 1;
+ if (!zoomed) {
+ limit = 2;
+ }
+ for (let y = tile_y - limit; y <= tile_y + limit; y++) {
+ if (y < 0 || y >= this.grid_size[1]) {
+ continue;
+ }
+ for (let x = tile_x - limit; x <= tile_x + limit; x++) {
+ if (x < 0 || x >= this.grid_size[0]) {
+ continue;
+ }
+ if (
+ this.tile_is_on_screen(
+ x,
+ y,
+ local_x,
+ local_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ )
+ ) {
+ // let colors = [
+ // [0, 0, 0],
+ // [0, 0, 1],
+ // [0, 1, 0],
+ // [0, 1, 1],
+ // [1, 0, 0],
+ // [1, 0, 1],
+ // [1, 1, 0],
+ // [1, 1, 0.5],
+ // [0.5, 0, 0.5],
+ // [0, 0.5, 0.5],
+ // ];
+ if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) {
+ this.display_thick_tile(
+ x,
+ y,
+ local_x,
+ local_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ );
+ } else {
+ this.display_tile(
+ x,
+ y,
+ local_x,
+ local_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ );
+ }
+ }
+ }
+ }
}
- if (on_screen_center_x - scaled_side >= width) {
- return false;
+
+ tile_is_on_screen(
+ tile_x,
+ tile_y,
+ current_x,
+ current_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ ) {
+ let width = g.getWidth();
+ let height = g.getHeight();
+ let center_x = width / 2;
+ let center_y = height / 2 + Y_OFFSET;
+ let side = this.side;
+ let tile_center_x = (tile_x + 0.5) * side;
+ let tile_center_y = (tile_y + 0.5) * side;
+ let scaled_center_x = (tile_center_x - current_x) * scale_factor;
+ let scaled_center_y = (tile_center_y - current_y) * scale_factor;
+ let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction;
+ let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction;
+ let on_screen_center_x = center_x - rotated_center_x;
+ let on_screen_center_y = center_y + rotated_center_y;
+
+ let scaled_side = side * scale_factor * Math.sqrt(1 / 2);
+
+ if (on_screen_center_x + scaled_side <= 0) {
+ return false;
+ }
+ if (on_screen_center_x - scaled_side >= width) {
+ return false;
+ }
+ if (on_screen_center_y + scaled_side <= 0) {
+ return false;
+ }
+ if (on_screen_center_y - scaled_side >= height) {
+ return false;
+ }
+ return true;
}
- if (on_screen_center_y + scaled_side <= 0) {
- return false;
+
+ tile_points(tile_num, tile_x, tile_y, scaled_side) {
+ let line_start_offset = this.tiles_offsets.tile_start_offset(
+ tile_y * this.grid_size[0]
+ );
+ let offset =
+ this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset;
+ let upper_limit =
+ this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset;
+
+ let line = this.binary_lines[tile_y];
+ // we need to copy both for correct results and for performances
+ // let's precompute also.
+ let cached_tile = new Float64Array(upper_limit - offset);
+ for (let i = offset; i < upper_limit; i += 2) {
+ let x = (tile_x + line.buffer[i] / 255) * scaled_side;
+ let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side;
+ cached_tile[i - offset] = x;
+ cached_tile[i + 1 - offset] = y;
+ }
+ return cached_tile;
}
- if (on_screen_center_y - scaled_side >= height) {
- return false;
+
+ invalidate_caches() {
+ this.points_cache = [];
}
- return true;
- }
- tile_points(tile_num, tile_x, tile_y, scaled_side) {
- let line_start_offset = this.tiles_offsets.tile_start_offset(
- tile_y * this.grid_size[0]
- );
- let offset =
- this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset;
- let upper_limit =
- this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset;
-
- let line = this.binary_lines[tile_y];
- // we need to copy both for correct results and for performances
- // let's precompute also.
- let cached_tile = new Float64Array(upper_limit - offset);
- for (let i = offset; i < upper_limit; i += 2) {
- let x = (tile_x + line.buffer[i] / 255) * scaled_side;
- let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side;
- cached_tile[i - offset] = x;
- cached_tile[i + 1 - offset] = y;
+ fetch_points(tile_x, tile_y, scaled_side) {
+ let tile_num = tile_x + tile_y * this.grid_size[0];
+ for (let i = 0; i < this.points_cache.length; i++) {
+ if (this.points_cache[i][0] == tile_num) {
+ return this.points_cache[i][1];
+ }
+ }
+ if (this.points_cache.length > 40) {
+ this.points_cache.shift();
+ }
+ let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side);
+ this.points_cache.push([tile_num, points]);
+ return points;
}
- return cached_tile;
- }
- invalidate_caches() {
- this.points_cache = [];
- }
+ display_tile(
+ tile_x,
+ tile_y,
+ current_x,
+ current_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ ) {
+ // "jit";
+ let center_x = g.getWidth() / 2;
+ let center_y = g.getHeight() / 2 + Y_OFFSET;
- fetch_points(tile_x, tile_y, scaled_side) {
- let tile_num = tile_x + tile_y * this.grid_size[0];
- for (let i = 0; i < this.points_cache.length; i++) {
- if (this.points_cache[i][0] == tile_num) {
- return this.points_cache[i][1];
- }
+ let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
+ let scaled_current_x = current_x * scale_factor;
+ let scaled_current_y = current_y * scale_factor;
+ let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]);
+ let c = cos_direction;
+ let s = sin_direction;
+ let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]);
+
+ for (let i = 0; i < screen_points.length; i += 4) {
+ g.drawLine(screen_points[i], screen_points[i + 1], screen_points[i + 2], screen_points[i + 3]);
+ }
}
- if (this.points_cache.length > 40) {
- this.points_cache.shift();
+
+ display_thick_tile(
+ tile_x,
+ tile_y,
+ current_x,
+ current_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ ) {
+ // "jit";
+ let center_x = g.getWidth() / 2;
+ let center_y = g.getHeight() / 2 + Y_OFFSET;
+
+ let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
+ let scaled_current_x = current_x * scale_factor;
+ let scaled_current_y = current_y * scale_factor;
+ let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]);
+ let c = cos_direction;
+ let s = sin_direction;
+ let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]);
+
+ for (let i = 0; i < screen_points.length; i += 4) {
+ let final_x = screen_points[i];
+ let final_y = screen_points[i + 1];
+ let new_final_x = screen_points[i + 2];
+ let new_final_y = screen_points[i + 3];
+
+ let xdiff = new_final_x - final_x;
+ let ydiff = new_final_y - final_y;
+ let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
+ let ox = (-ydiff / d) * 3;
+ let oy = (xdiff / d) * 3;
+ g.fillPoly([
+ final_x + ox,
+ final_y + oy,
+ new_final_x + ox,
+ new_final_y + oy,
+ new_final_x - ox,
+ new_final_y - oy,
+ final_x - ox,
+ final_y - oy,
+ ]);
+ }
}
- let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side);
- this.points_cache.push([tile_num, points]);
- return points;
- }
-
- display_tile(
- tile_x,
- tile_y,
- current_x,
- current_y,
- scale_factor,
- cos_direction,
- sin_direction
- ) {
- "jit";
- let center_x = g.getWidth() / 2;
- let center_y = g.getHeight() / 2 + Y_OFFSET;
-
- let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
- let scaled_current_x = current_x * scale_factor;
- let scaled_current_y = current_y * scale_factor;
-
- for (let i = 0; i < points.length; i += 4) {
- let scaled_x = points[i] - scaled_current_x;
- let scaled_y = points[i + 1] - scaled_current_y;
- let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let final_x = center_x - rotated_x;
- let final_y = center_y + rotated_y;
- scaled_x = points[i + 2] - scaled_current_x;
- scaled_y = points[i + 3] - scaled_current_y;
- rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let new_final_x = center_x - rotated_x;
- let new_final_y = center_y + rotated_y;
- g.drawLine(final_x, final_y, new_final_x, new_final_y);
- }
- }
-
- display_thick_tile(
- tile_x,
- tile_y,
- current_x,
- current_y,
- scale_factor,
- cos_direction,
- sin_direction
- ) {
- let center_x = g.getWidth() / 2;
- let center_y = g.getHeight() / 2 + Y_OFFSET;
-
- let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
- let scaled_current_x = current_x * scale_factor;
- let scaled_current_y = current_y * scale_factor;
-
- for (let i = 0; i < points.length; i += 4) {
- let scaled_x = points[i] - scaled_current_x;
- let scaled_y = points[i + 1] - scaled_current_y;
- let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let final_x = center_x - rotated_x;
- let final_y = center_y + rotated_y;
- scaled_x = points[i + 2] - scaled_current_x;
- scaled_y = points[i + 3] - scaled_current_y;
- rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let new_final_x = center_x - rotated_x;
- let new_final_y = center_y + rotated_y;
-
- let xdiff = new_final_x - final_x;
- let ydiff = new_final_y - final_y;
- let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
- let ox = (-ydiff / d) * 3;
- let oy = (xdiff / d) * 3;
- g.fillPoly([
- final_x + ox,
- final_y + oy,
- new_final_x + ox,
- new_final_y + oy,
- new_final_x - ox,
- new_final_y - oy,
- final_x - ox,
- final_y - oy,
- ]);
- }
- }
}
class Interests {
- constructor(buffer, offset) {
- this.first_tile = Uint32Array(buffer, offset, 2); // absolute tile id of first tile
- offset += 2 * 4;
- this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height
- offset += 2 * 4;
- this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates
- offset += 2 * 8;
- let side_array = Float64Array(buffer, offset, 1); // side of a tile
- this.side = side_array[0];
- offset += 8;
+ constructor(buffer, offset) {
+ this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile
+ offset += 2 * 4;
+ this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height
+ offset += 2 * 4;
+ this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates
+ offset += 2 * 8;
+ let side_array = Float64Array(buffer, offset, 1); // side of a tile
+ this.side = side_array[0];
+ offset += 8;
- let res = new TilesOffsets(buffer, offset);
- offset = res[1];
- this.offsets = res[0];
- let end = this.offsets.end_offset();
- this.binary_interests = new Uint8Array(end);
- let binary_interests = Uint8Array(buffer, offset, end);
- for (let i = 0; i < end; i++) {
- this.binary_interests[i] = binary_interests[i];
- }
- offset += end;
- this.points_cache = [];
- return [this, offset];
- }
-
- display(
- displayed_x,
- displayed_y,
- scale_factor,
- cos_direction,
- sin_direction
- ) {
- let local_x = displayed_x - this.start_coordinates[0];
- let local_y = displayed_y - this.start_coordinates[1];
- let tile_x = Math.floor(local_x / this.side);
- let tile_y = Math.floor(local_y / this.side);
- for (let y = tile_y - 1; y <= tile_y + 1; y++) {
- if (y < 0 || y >= this.grid_size[1]) {
- continue;
- }
- for (let x = tile_x - 1; x <= tile_x + 1; x++) {
- if (x < 0 || x >= this.grid_size[0]) {
- continue;
+ let res = new TilesOffsets(buffer, offset);
+ offset = res[1];
+ this.offsets = res[0];
+ let end = this.offsets.end_offset();
+ this.binary_interests = new Uint8Array(end);
+ let binary_interests = Uint8Array(buffer, offset, end);
+ for (let i = 0; i < end; i++) {
+ this.binary_interests[i] = binary_interests[i];
}
- this.display_tile(
- x,
- y,
- local_x,
- local_y,
- scale_factor,
- cos_direction,
- sin_direction
- );
- }
+ offset += end;
+ this.points_cache = [];
+ return [this, offset];
}
- }
- tile_points(tile_num, tile_x, tile_y, scaled_side) {
- let offset = this.offsets.tile_start_offset(tile_num);
- let upper_limit = this.offsets.tile_end_offset(tile_num);
-
- let tile_interests = [];
- for (let i = offset; i < upper_limit; i += 3) {
- let interest = this.binary_interests[i];
- let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side;
- let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side;
- if (interest >= interests_colors.length) {
- throw "bad interest" + interest + "at" + tile_num + "offset" + i;
- }
- tile_interests.push(interest);
- tile_interests.push(x);
- tile_interests.push(y);
+ display(
+ displayed_x,
+ displayed_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ ) {
+ let local_x = displayed_x - this.start_coordinates[0];
+ let local_y = displayed_y - this.start_coordinates[1];
+ let tile_x = Math.floor(local_x / this.side);
+ let tile_y = Math.floor(local_y / this.side);
+ for (let y = tile_y - 1; y <= tile_y + 1; y++) {
+ if (y < 0 || y >= this.grid_size[1]) {
+ continue;
+ }
+ for (let x = tile_x - 1; x <= tile_x + 1; x++) {
+ if (x < 0 || x >= this.grid_size[0]) {
+ continue;
+ }
+ this.display_tile(
+ x,
+ y,
+ local_x,
+ local_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ );
+ }
+ }
}
- return tile_interests;
- }
- fetch_points(tile_x, tile_y, scaled_side) {
- //TODO: factorize with map ?
- let tile_num = tile_x + tile_y * this.grid_size[0];
- for (let i = 0; i < this.points_cache.length; i++) {
- if (this.points_cache[i][0] == tile_num) {
- return this.points_cache[i][1];
- }
+
+ tile_points(tile_num, tile_x, tile_y, scaled_side) {
+ let offset = this.offsets.tile_start_offset(tile_num);
+ let upper_limit = this.offsets.tile_end_offset(tile_num);
+
+ let tile_interests = [];
+ for (let i = offset; i < upper_limit; i += 3) {
+ let interest = this.binary_interests[i];
+ let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side;
+ let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side;
+ if (interest >= interests_colors.length) {
+ throw "bad interest" + interest + "at" + tile_num + "offset" + i;
+ }
+ tile_interests.push(interest);
+ tile_interests.push(x);
+ tile_interests.push(y);
+ }
+ return tile_interests;
}
- if (this.points_cache.length > 40) {
- this.points_cache.shift();
+ fetch_points(tile_x, tile_y, scaled_side) {
+ //TODO: factorize with map ?
+ let tile_num = tile_x + tile_y * this.grid_size[0];
+ for (let i = 0; i < this.points_cache.length; i++) {
+ if (this.points_cache[i][0] == tile_num) {
+ return this.points_cache[i][1];
+ }
+ }
+ if (this.points_cache.length > 40) {
+ this.points_cache.shift();
+ }
+ let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side);
+ this.points_cache.push([tile_num, points]);
+ return points;
}
- let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side);
- this.points_cache.push([tile_num, points]);
- return points;
- }
- invalidate_caches() {
- this.points_cache = [];
- }
- display_tile(
- tile_x,
- tile_y,
- displayed_x,
- displayed_y,
- scale_factor,
- cos_direction,
- sin_direction
- ) {
- let width = g.getWidth();
- let half_width = width / 2;
- let half_height = g.getHeight() / 2 + Y_OFFSET;
- let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
-
- let scaled_current_x = displayed_x * scale_factor;
- let scaled_current_y = displayed_y * scale_factor;
-
- for (let i = 0; i < interests.length; i += 3) {
- let type = interests[i];
- let x = interests[i + 1];
- let y = interests[i + 2];
-
- let scaled_x = x - scaled_current_x;
- let scaled_y = y - scaled_current_y;
- let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let final_x = half_width - rotated_x;
- let final_y = half_height + rotated_y;
-
- let color = interests_colors[type];
- if (type == 0) {
- g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6);
- }
- g.setColor(color).fillCircle(final_x, final_y, 5);
+ invalidate_caches() {
+ this.points_cache = [];
+ }
+ display_tile(
+ tile_x,
+ tile_y,
+ displayed_x,
+ displayed_y,
+ scale_factor,
+ cos_direction,
+ sin_direction
+ ) {
+ let width = g.getWidth();
+ let half_width = width / 2;
+ let half_height = g.getHeight() / 2 + Y_OFFSET;
+ let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
+
+ let scaled_current_x = displayed_x * scale_factor;
+ let scaled_current_y = displayed_y * scale_factor;
+
+ for (let i = 0; i < interests.length; i += 3) {
+ let type = interests[i];
+ let x = interests[i + 1];
+ let y = interests[i + 2];
+
+ let scaled_x = x - scaled_current_x;
+ let scaled_y = y - scaled_current_y;
+ let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
+ let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
+ let final_x = half_width - rotated_x;
+ let final_y = half_height + rotated_y;
+
+ let color = interests_colors[type];
+ if (type == 0) {
+ g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6);
+ }
+ g.setColor(color).fillCircle(final_x, final_y, 5);
+ }
}
- }
}
class Status {
- constructor(path, maps, interests) {
- this.path = path;
- this.maps = maps;
- this.interests = interests;
- let half_screen_width = g.getWidth() / 2;
- let half_screen_height = g.getHeight() / 2;
- let half_screen_diagonal = Math.sqrt(
- half_screen_width * half_screen_width +
- half_screen_height * half_screen_height
- );
- this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates
- this.on_path = true; // are we on the path or lost ?
- this.position = null; // where we are
- this.adjusted_cos_direction = 1; // cos of where we look at
- this.adjusted_sin_direction = 0; // sin of where we look at
- this.current_segment = null; // which segment is closest
- this.reaching = null; // which waypoint are we reaching ?
- this.distance_to_next_point = null; // how far are we from next point ?
- this.projected_point = null;
-
- if (this.path !== null) {
- let r = [0];
- // let's do a reversed prefix computations on all distances:
- // loop on all segments in reversed order
- let previous_point = null;
- for (let i = this.path.len - 1; i >= 0; i--) {
- let point = this.path.point(i);
- if (previous_point !== null) {
- r.unshift(r[0] + point.distance(previous_point));
- }
- previous_point = point;
- }
- this.remaining_distances = r; // how much distance remains at start of each segment
- }
- this.starting_time = null; // time we start
- this.advanced_distance = 0.0;
- this.gps_coordinates_counter = 0; // how many coordinates did we receive
- this.old_points = []; // record previous points but only when enough distance between them
- this.old_times = []; // the corresponding times
- }
- invalidate_caches() {
- for (let i = 0; i < this.maps.length; i++) {
- this.maps[i].invalidate_caches();
- }
- if (this.interests !== null) {
- this.interests.invalidate_caches();
- }
- }
- new_position_reached(position) {
- // we try to figure out direction by looking at previous points
- // instead of the gps course which is not very nice.
-
- let now = getTime();
-
- if (this.old_points.length == 0) {
- this.gps_coordinates_counter += 1;
- this.old_points.push(position);
- this.old_times.push(now);
- return null;
- } else {
- let previous_point = this.old_points[this.old_points.length - 1];
- let distance_to_previous = previous_point.distance(position);
- // gps signal is noisy but rarely above 5 meters
- if (distance_to_previous < 5) {
- return null;
- }
- }
- this.gps_coordinates_counter += 1;
- this.old_points.push(position);
- this.old_times.push(now);
-
- let oldest_point = this.old_points[0];
- let distance_to_oldest = oldest_point.distance(position);
-
- // every 3 points we count the distance
- if (this.gps_coordinates_counter % 3 == 0) {
- if (distance_to_oldest < 150.0) {
- // to avoid gps glitches
- this.advanced_distance += distance_to_oldest;
- }
- }
-
- this.instant_speed = distance_to_oldest / (now - this.old_times[0]);
-
- if (this.old_points.length == 4) {
- this.old_points.shift();
- this.old_times.shift();
- }
- // let's just take angle of segment between newest point and a point a bit before
- let previous_index = this.old_points.length - 3;
- if (previous_index < 0) {
- previous_index = 0;
- }
- let diff = position.minus(this.old_points[previous_index]);
- let angle = Math.atan2(diff.lat, diff.lon);
- return angle;
- }
- update_position(new_position, maybe_direction, timestamp) {
- let direction = this.new_position_reached(new_position);
- if (direction === null) {
- if (maybe_direction === null) {
- return;
- } else {
- direction = maybe_direction;
- }
- }
- if (in_menu) {
- return;
- }
-
- this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0);
- this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0);
- this.angle = direction;
- let cos_direction = Math.cos(direction);
- let sin_direction = Math.sin(direction);
- this.position = new_position;
-
- // we will display position of where we'll be at in a few seconds
- // and not where we currently are.
- // this is because the display has more than 1sec duration.
- this.displayed_position = new Point(
- new_position.lon + cos_direction * this.instant_speed * 0.00001,
- new_position.lat + sin_direction * this.instant_speed * 0.00001
- );
-
- // abort if we are late
- // if (timestamp !== null) {
- // let elapsed = Date.now() - timestamp;
- // if (elapsed > 1000) {
- // console.log("we are late");
- // return;
- // }
- // console.log("we are not late");
- // }
-
- if (this.path !== null) {
- // detect segment we are on now
- let res = this.path.nearest_segment(
- this.displayed_position,
- Math.max(0, this.current_segment - 1),
- Math.min(this.current_segment + 2, this.path.len - 1),
- cos_direction,
- sin_direction
- );
- let orientation = res[0];
- let next_segment = res[1];
-
- if (this.is_lost(next_segment)) {
- // start_profiling();
- // it did not work, try anywhere
- res = this.path.nearest_segment(
- this.displayed_position,
- 0,
- this.path.len - 1,
- cos_direction,
- sin_direction
+ constructor(path, maps, interests) {
+ this.path = path;
+ this.default_options = true; // do we still have default options ?
+ this.active = false; // should we have screen on
+ this.last_activity = getTime();
+ this.maps = maps;
+ this.interests = interests;
+ let half_screen_width = g.getWidth() / 2;
+ let half_screen_height = g.getHeight() / 2;
+ let half_screen_diagonal = Math.sqrt(
+ half_screen_width * half_screen_width +
+ half_screen_height * half_screen_height
);
- orientation = res[0];
- next_segment = res[1];
- // end_profiling("repositioning");
- }
- // now check if we strayed away from path or back to it
- let lost = this.is_lost(next_segment);
- if (this.on_path == lost) {
- // if status changes
- if (lost) {
- Bangle.buzz(); // we lost path
- setTimeout(() => Bangle.buzz(), 500);
- setTimeout(() => Bangle.buzz(), 1000);
- setTimeout(() => Bangle.buzz(), 1500);
+ this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates
+ this.on_path = true; // are we on the path or lost ?
+ this.position = null; // where we are
+ this.adjusted_cos_direction = 1; // cos of where we look at
+ this.adjusted_sin_direction = 0; // sin of where we look at
+ this.current_segment = null; // which segment is closest
+ this.reaching = null; // which waypoint are we reaching ?
+ this.distance_to_next_point = null; // how far are we from next point ?
+ this.projected_point = null;
+
+ if (this.path !== null) {
+ let r = [0];
+ // let's do a reversed prefix computations on all distances:
+ // loop on all segments in reversed order
+ let previous_point = null;
+ for (let i = this.path.len - 1; i >= 0; i--) {
+ let point = this.path.point(i);
+ if (previous_point !== null) {
+ r.unshift(r[0] + point.distance(previous_point));
+ }
+ previous_point = point;
+ }
+ this.remaining_distances = r; // how much distance remains at start of each segment
}
- this.on_path = !lost;
- }
-
- this.current_segment = next_segment;
-
- // check if we are nearing the next point on our path and alert the user
- let next_point = this.current_segment + (1 - orientation);
- this.distance_to_next_point = Math.ceil(
- this.position.distance(this.path.point(next_point))
- );
-
- // disable gps when far from next point and locked
- // if (Bangle.isLocked() && !settings.keep_gps_alive) {
- // let time_to_next_point =
- // (this.distance_to_next_point * 3.6) / settings.max_speed;
- // if (time_to_next_point > 60) {
- // Bangle.setGPSPower(false, "gipy");
- // setTimeout(function () {
- // Bangle.setGPSPower(true, "gipy");
- // }, time_to_next_point);
- // }
- // }
- if (this.reaching != next_point && this.distance_to_next_point <= 100) {
- this.reaching = next_point;
- let reaching_waypoint = this.path.is_waypoint(next_point);
- if (reaching_waypoint) {
- Bangle.buzz();
- setTimeout(() => Bangle.buzz(), 500);
- setTimeout(() => Bangle.buzz(), 1000);
- setTimeout(() => Bangle.buzz(), 1500);
- if (Bangle.isLocked()) {
- Bangle.setLocked(false);
- }
- }
- }
+ this.starting_time = null; // time we start
+ this.advanced_distance = 0.0;
+ this.gps_coordinates_counter = 0; // how many coordinates did we receive
+ this.old_points = []; // record previous points but only when enough distance between them
+ this.old_times = []; // the corresponding times
}
-
- // abort most frames if locked
- if (Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) {
- return;
- }
-
- // re-display
- this.display();
- }
- display_direction() {
- //TODO: go towards point on path at 20 meter
- if (this.current_segment === null) {
- return;
- }
- let next_point = this.path.point(this.current_segment + (1 - go_backwards));
-
- let distance_to_next_point = Math.ceil(
- this.projected_point.distance(next_point)
- );
- let towards;
- if (distance_to_next_point < 20) {
- towards = this.path.point(this.current_segment + 2 * (1 - go_backwards));
- } else {
- towards = next_point;
- }
- let diff = towards.minus(this.projected_point);
- direction = Math.atan2(diff.lat, diff.lon);
-
- let full_angle = direction - this.angle;
- // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor);
- // g.setColor(1,0,1).fillCircle(c[0], c[1], 5);
-
- let scale;
- if (zoomed) {
- scale = this.scale_factor;
- } else {
- scale = this.scale_factor / 2;
- }
-
- c = this.projected_point.coordinates(
- this.displayed_position,
- this.adjusted_cos_direction,
- this.adjusted_sin_direction,
- scale
- );
-
- let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2);
- let cos2 = Math.cos(full_angle + Math.PI / 2);
- let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2);
- let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2);
- let sin2 = Math.sin(-full_angle - Math.PI / 2);
- let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2);
- g.setColor(0, 1, 0).fillPoly([
- c[0] + cos1 * 15,
- c[1] + sin1 * 15,
- c[0] + cos2 * 20,
- c[1] + sin2 * 20,
- c[0] + cos3 * 15,
- c[1] + sin3 * 15,
- c[0] + cos3 * 10,
- c[1] + sin3 * 10,
- c[0] + cos2 * 15,
- c[1] + sin2 * 15,
- c[0] + cos1 * 10,
- c[1] + sin1 * 10,
- ]);
- }
- remaining_distance() {
- let remaining_in_correct_orientation =
- this.remaining_distances[this.current_segment + 1] +
- this.position.distance(this.path.point(this.current_segment + 1));
-
- if (go_backwards) {
- return this.remaining_distances[0] - remaining_in_correct_orientation;
- } else {
- return remaining_in_correct_orientation;
- }
- }
- // check if we are lost (too far from segment we think we are on)
- // if we are adjust scale so that path will still be displayed.
- // we do the scale adjustment here to avoid recomputations later on.
- is_lost(segment) {
- let projection = this.displayed_position.closest_segment_point(
- this.path.point(segment),
- this.path.point(segment + 1)
- );
- this.projected_point = projection; // save this info for display
- let distance_to_projection = this.displayed_position.distance(projection);
- if (distance_to_projection > settings.lost_distance) {
- return true;
- } else {
- return false;
- }
- }
- display() {
- if (displaying || in_menu) {
- return; // don't draw on drawings
- }
- displaying = true;
- g.clear();
- let scale_factor = this.scale_factor;
- if (!zoomed) {
- scale_factor /= 2;
- }
-
- // start_profiling();
- for (let i = 0; i < this.maps.length; i++) {
- this.maps[i].display(
- this.displayed_position.lon,
- this.displayed_position.lat,
- scale_factor,
- this.adjusted_cos_direction,
- this.adjusted_sin_direction
- );
- }
- // end_profiling("map");
- if (this.interests !== null) {
- this.interests.display(
- this.displayed_position.lon,
- this.displayed_position.lat,
- scale_factor,
- this.adjusted_cos_direction,
- this.adjusted_sin_direction
- );
- }
- if (this.position !== null) {
- this.display_path();
- }
-
- this.display_direction();
- this.display_stats();
- Bangle.drawWidgets();
- displaying = false;
- }
- display_stats() {
- let now = new Date();
- let minutes = now.getMinutes().toString();
- if (minutes.length < 2) {
- minutes = "0" + minutes;
- }
- let hours = now.getHours().toString();
-
- // display the clock
- g.setFont("6x8:2")
- .setFontAlign(-1, -1, 0)
- .setColor(g.theme.fg)
- .drawString(hours + ":" + minutes, 0, 24);
-
- let approximate_speed;
- // display speed (avg and instant)
- if (this.old_times.length > 0) {
- let point_time = this.old_times[this.old_times.length - 1];
- let done_in = point_time - this.starting_time;
- approximate_speed = Math.round(
- (this.advanced_distance * 3.6) / done_in
- );
- let approximate_instant_speed = Math.round(this.instant_speed * 3.6);
- g.setFont("6x8:2")
- .setFontAlign(-1, -1, 0)
- .drawString(
- "" +
- approximate_speed +
- "km/h (in." +
- approximate_instant_speed +
- ")",
- 0,
- g.getHeight() - 15
- );
- }
-
- if (this.path === null || this.position === null) {
- return;
- }
-
- let remaining_distance = this.remaining_distance();
- let rounded_distance = Math.round(remaining_distance / 100) / 10;
- let total = Math.round(this.remaining_distances[0] / 100) / 10;
- // now, distance to next point in meters
- g.setFont("6x8:2")
- .setFontAlign(-1, -1, 0)
- .setColor(g.theme.fg)
- .drawString(
- "" + this.distance_to_next_point + "m",
- 0,
- g.getHeight() - 49
- );
-
- let forward_eta = compute_eta(
- now.getHours(),
- now.getMinutes(),
- approximate_speed,
- remaining_distance / 1000
- );
-
- // now display ETA
- g.setFont("6x8:2")
- .setFontAlign(-1, -1, 0)
- .setColor(g.theme.fg)
- .drawString(forward_eta, 0, 42);
-
- // display distance on path
- g.setFont("6x8:2").drawString(
- "" + rounded_distance + "/" + total,
- 0,
- g.getHeight() - 32
- );
-
- // display various indicators
- if (this.distance_to_next_point <= 100) {
- if (this.path.is_waypoint(this.reaching)) {
- g.setColor(0.0, 1.0, 0.0)
- .setFont("6x15")
- .drawString("turn", g.getWidth() - 50, 30);
- }
- }
- if (!this.on_path) {
- g.setColor(1.0, 0.0, 0.0)
- .setFont("6x15")
- .drawString("lost", g.getWidth() - 55, 35);
- }
- }
- display_path() {
- // don't display all segments, only those neighbouring current segment
- // this is most likely to be the correct display
- // while lowering the cost a lot
- //
- // note that all code is inlined here to speed things up
- let cos = this.adjusted_cos_direction;
- let sin = this.adjusted_sin_direction;
- let displayed_x = this.displayed_position.lon;
- let displayed_y = this.displayed_position.lat;
- let width = g.getWidth();
- let height = g.getHeight();
- let half_width = width / 2;
- let half_height = height / 2 + Y_OFFSET;
- let scale_factor = this.scale_factor;
- if (!zoomed) {
- scale_factor /= 2;
- }
-
- if (this.path !== null) {
- // compute coordinate for projection on path
- let tx = (this.projected_point.lon - displayed_x) * scale_factor;
- let ty = (this.projected_point.lat - displayed_y) * scale_factor;
- let rotated_x = tx * cos - ty * sin;
- let rotated_y = tx * sin + ty * cos;
- let projected_x = half_width - Math.round(rotated_x); // x is inverted
- let projected_y = half_height + Math.round(rotated_y);
-
- // display direction to next point if lost
- if (!this.on_path) {
- let next_point = this.path.point(this.current_segment + 1);
- let previous_point = this.path.point(this.current_segment);
- let nearest_point;
- if (
- previous_point.fake_distance(this.position) <
- next_point.fake_distance(this.position)
- ) {
- nearest_point = previous_point;
+ activate() {
+ this.last_activity = getTime();
+ if (this.active) {
+ return;
} else {
- nearest_point = next_point;
+ this.active = true;
+ Bangle.setLCDBrightness(settings.brightness);
+ Bangle.setLocked(false);
+ if (settings.power_lcd_off) {
+ Bangle.setLCDPower(true);
+ }
}
- let tx = (nearest_point.lon - displayed_x) * scale_factor;
- let ty = (nearest_point.lat - displayed_y) * scale_factor;
- let rotated_x = tx * cos - ty * sin;
- let rotated_y = tx * sin + ty * cos;
- let x = half_width - Math.round(rotated_x); // x is inverted
- let y = half_height + Math.round(rotated_y);
- g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y);
- }
-
- // display current-segment's projection
- g.setColor(0, 0, 0);
- g.fillCircle(projected_x, projected_y, 4);
}
+ check_activity() {
+ if (!this.active || !powersaving) {
+ return;
+ }
+ if (getTime() - this.last_activity > 30) {
+ this.active = false;
+ Bangle.setLCDBrightness(0);
+ if (settings.power_lcd_off) {
+ Bangle.setLCDPower(false);
+ }
+ }
+ }
+ invalidate_caches() {
+ for (let i = 0; i < this.maps.length; i++) {
+ this.maps[i].invalidate_caches();
+ }
+ if (this.interests !== null) {
+ this.interests.invalidate_caches();
+ }
+ }
+ new_position_reached(position) {
+ // we try to figure out direction by looking at previous points
+ // instead of the gps course which is not very nice.
- // now display ourselves
- g.setColor(0, 0, 0);
- g.fillCircle(half_width, half_height, 5);
- }
+ let now = getTime();
+
+ if (this.old_points.length == 0) {
+ this.gps_coordinates_counter += 1;
+ this.old_points.push(position);
+ this.old_times.push(now);
+ return null;
+ } else {
+ let previous_point = this.old_points[this.old_points.length - 1];
+ let distance_to_previous = previous_point.distance(position);
+ // gps signal is noisy but rarely above 5 meters
+ if (distance_to_previous < 5) {
+ // update instant speed and return
+ let oldest_point = this.old_points[0];
+ let distance_to_oldest = oldest_point.distance(position);
+ this.instant_speed = distance_to_oldest / (now - this.old_times[0]);
+ return null;
+ }
+ }
+ this.gps_coordinates_counter += 1;
+ this.old_points.push(position);
+ this.old_times.push(now);
+
+ let oldest_point = this.old_points[0];
+ let distance_to_oldest = oldest_point.distance(position);
+
+ // every 3 points we count the distance
+ if (this.gps_coordinates_counter % 3 == 0) {
+ if (distance_to_oldest < 150.0) {
+ // to avoid gps glitches
+ this.advanced_distance += distance_to_oldest;
+ }
+ }
+
+ this.instant_speed = distance_to_oldest / (now - this.old_times[0]);
+
+ if (this.old_points.length == 4) {
+ this.old_points.shift();
+ this.old_times.shift();
+ }
+ // let's just take angle of segment between newest point and a point a bit before
+ let previous_index = this.old_points.length - 3;
+ if (previous_index < 0) {
+ previous_index = 0;
+ }
+ let diff = position.minus(this.old_points[previous_index]);
+ let angle = Math.atan2(diff.lat, diff.lon);
+ return angle;
+ }
+ update_position(new_position) {
+ let direction = this.new_position_reached(new_position);
+ if (direction === null) {
+ if (this.old_points.length > 1) {
+ this.display(); // re-display because speed has changed
+ }
+ return;
+ }
+ if (in_menu) {
+ return;
+ }
+ if (this.instant_speed * 3.6 < 13) {
+ this.activate(); // if we go too slow turn on, we might be looking for the direction to follow
+ if (!this.default_options) {
+ this.default_options = true;
+
+ Bangle.setOptions({
+ lockTimeout: 10000,
+ backlightTimeout: 10000,
+ wakeOnTwist: true,
+ powerSave: true,
+ });
+ }
+ } else {
+ if (this.default_options) {
+ this.default_options = false;
+
+ Bangle.setOptions({
+ lockTimeout: 0,
+ backlightTimeout: 0,
+ lcdPowerTimeout: 0,
+ hrmSportMode: 2,
+ wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds.
+ wakeOnFaceUp: false,
+ wakeOnTouch: true,
+ powerSave: false,
+ });
+ Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000)
+ }
+
+ }
+ this.check_activity(); // if we don't move or are in menu we should stay on
+
+ this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0);
+ this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0);
+ this.angle = direction;
+ let cos_direction = Math.cos(direction);
+ let sin_direction = Math.sin(direction);
+ this.position = new_position;
+
+ // we will display position of where we'll be at in a few seconds
+ // and not where we currently are.
+ // this is because the display has more than 1sec duration.
+ this.displayed_position = new Point(
+ new_position.lon + cos_direction * this.instant_speed * 0.00001,
+ new_position.lat + sin_direction * this.instant_speed * 0.00001
+ );
+
+ if (this.path !== null) {
+ // detect segment we are on now
+ let res = this.path.nearest_segment(
+ this.displayed_position,
+ Math.max(0, this.current_segment - 1),
+ Math.min(this.current_segment + 2, this.path.len - 1),
+ cos_direction,
+ sin_direction
+ );
+ let orientation = res[0];
+ let next_segment = res[1];
+
+ if (this.is_lost(next_segment)) {
+ // start_profiling();
+ // it did not work, try anywhere
+ res = this.path.nearest_segment(
+ this.displayed_position,
+ 0,
+ this.path.len - 1,
+ cos_direction,
+ sin_direction
+ );
+ orientation = res[0];
+ next_segment = res[1];
+ // end_profiling("repositioning");
+ }
+ // now check if we strayed away from path or back to it
+ let lost = this.is_lost(next_segment);
+ if (this.on_path == lost) {
+ this.activate();
+ // if status changes
+ if (lost) {
+ Bangle.buzz(); // we lost path
+ setTimeout(() => Bangle.buzz(), 500);
+ setTimeout(() => Bangle.buzz(), 1000);
+ setTimeout(() => Bangle.buzz(), 1500);
+ }
+ this.on_path = !lost;
+ }
+
+ this.current_segment = next_segment;
+
+ // check if we are nearing the next point on our path and alert the user
+ let next_point = this.current_segment + (1 - orientation);
+ this.distance_to_next_point = Math.ceil(
+ this.position.distance(this.path.point(next_point))
+ );
+
+ // disable gps when far from next point and locked
+ // if (Bangle.isLocked() && !settings.keep_gps_alive) {
+ // let time_to_next_point =
+ // (this.distance_to_next_point * 3.6) / settings.max_speed;
+ // if (time_to_next_point > 60) {
+ // Bangle.setGPSPower(false, "gipy");
+ // setTimeout(function () {
+ // Bangle.setGPSPower(true, "gipy");
+ // }, time_to_next_point);
+ // }
+ // }
+ if (this.reaching != next_point && this.distance_to_next_point <= 100) {
+ this.activate();
+ this.reaching = next_point;
+ let reaching_waypoint = this.path.is_waypoint(next_point);
+ if (reaching_waypoint) {
+ if (settings.buzz_on_turns) {
+ Bangle.buzz();
+ setTimeout(() => Bangle.buzz(), 500);
+ setTimeout(() => Bangle.buzz(), 1000);
+ setTimeout(() => Bangle.buzz(), 1500);
+ }
+ }
+ }
+ }
+
+ // abort most frames if inactive
+ if (!this.active && this.gps_coordinates_counter % 5 != 0) {
+ return;
+ }
+
+ // re-display
+ this.display();
+ }
+ display_direction() {
+ //TODO: go towards point on path at 20 meter
+ if (this.current_segment === null) {
+ return;
+ }
+ let next_point = this.path.point(this.current_segment + (1 - go_backwards));
+
+ let distance_to_next_point = Math.ceil(
+ this.projected_point.distance(next_point)
+ );
+ let towards;
+ if (distance_to_next_point < 20) {
+ towards = this.path.point(this.current_segment + 2 * (1 - go_backwards));
+ } else {
+ towards = next_point;
+ }
+ let diff = towards.minus(this.projected_point);
+ direction = Math.atan2(diff.lat, diff.lon);
+
+ let full_angle = direction - this.angle;
+ // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor);
+ // g.setColor(1,0,1).fillCircle(c[0], c[1], 5);
+
+ let scale;
+ if (zoomed) {
+ scale = this.scale_factor;
+ } else {
+ scale = this.scale_factor / 2;
+ }
+
+ c = this.projected_point.coordinates(
+ this.displayed_position,
+ this.adjusted_cos_direction,
+ this.adjusted_sin_direction,
+ scale
+ );
+
+ let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2);
+ let cos2 = Math.cos(full_angle + Math.PI / 2);
+ let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2);
+ let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2);
+ let sin2 = Math.sin(-full_angle - Math.PI / 2);
+ let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2);
+ g.setColor(0, 1, 0).fillPoly([
+ c[0] + cos1 * 15,
+ c[1] + sin1 * 15,
+ c[0] + cos2 * 20,
+ c[1] + sin2 * 20,
+ c[0] + cos3 * 15,
+ c[1] + sin3 * 15,
+ c[0] + cos3 * 10,
+ c[1] + sin3 * 10,
+ c[0] + cos2 * 15,
+ c[1] + sin2 * 15,
+ c[0] + cos1 * 10,
+ c[1] + sin1 * 10,
+ ]);
+ }
+ remaining_distance() {
+ let remaining_in_correct_orientation =
+ this.remaining_distances[this.current_segment + 1] +
+ this.position.distance(this.path.point(this.current_segment + 1));
+
+ if (go_backwards) {
+ return this.remaining_distances[0] - remaining_in_correct_orientation;
+ } else {
+ return remaining_in_correct_orientation;
+ }
+ }
+ // check if we are lost (too far from segment we think we are on)
+ // if we are adjust scale so that path will still be displayed.
+ // we do the scale adjustment here to avoid recomputations later on.
+ is_lost(segment) {
+ let projection = this.displayed_position.closest_segment_point(
+ this.path.point(segment),
+ this.path.point(segment + 1)
+ );
+ this.projected_point = projection; // save this info for display
+ let distance_to_projection = this.displayed_position.distance(projection);
+ if (distance_to_projection > settings.lost_distance) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ display() {
+ if (displaying || in_menu) {
+ return; // don't draw on drawings
+ }
+ displaying = true;
+ g.clear();
+ let scale_factor = this.scale_factor;
+ if (!zoomed) {
+ scale_factor /= 2;
+ }
+
+ // start_profiling();
+ for (let i = 0; i < this.maps.length; i++) {
+ this.maps[i].display(
+ this.displayed_position.lon,
+ this.displayed_position.lat,
+ scale_factor,
+ this.adjusted_cos_direction,
+ this.adjusted_sin_direction
+ );
+ }
+ // end_profiling("map");
+ if (this.interests !== null) {
+ this.interests.display(
+ this.displayed_position.lon,
+ this.displayed_position.lat,
+ scale_factor,
+ this.adjusted_cos_direction,
+ this.adjusted_sin_direction
+ );
+ }
+ if (this.position !== null) {
+ this.display_path();
+ }
+
+ this.display_direction();
+ this.display_stats();
+ Bangle.drawWidgets();
+ displaying = false;
+ }
+ display_stats() {
+ let now = new Date();
+ let minutes = now.getMinutes().toString();
+ if (minutes.length < 2) {
+ minutes = "0" + minutes;
+ }
+ let hours = now.getHours().toString();
+
+ // display the clock
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .setColor(g.theme.fg)
+ .drawString(hours + ":" + minutes, 0, 24);
+
+ let approximate_speed;
+ // display speed (avg and instant)
+ if (this.old_times.length > 0) {
+ let point_time = this.old_times[this.old_times.length - 1];
+ let done_in = point_time - this.starting_time;
+ approximate_speed = Math.round(
+ (this.advanced_distance * 3.6) / done_in
+ );
+ let approximate_instant_speed = Math.round(this.instant_speed * 3.6);
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .drawString(
+ "" +
+ approximate_speed +
+ "km/h",
+ 0,
+ g.getHeight() - 15
+ );
+
+ g.setFont("6x8:3")
+ .setFontAlign(1, -1, 0)
+ .drawString(
+ "" + approximate_instant_speed,
+ g.getWidth(),
+ g.getHeight() - 22
+ );
+ }
+
+ if (this.path === null || this.position === null) {
+ return;
+ }
+
+ let remaining_distance = this.remaining_distance();
+ let rounded_distance = Math.round(remaining_distance / 100) / 10;
+ let total = Math.round(this.remaining_distances[0] / 100) / 10;
+ // now, distance to next point in meters
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .setColor(g.theme.fg)
+ .drawString(
+ "" + this.distance_to_next_point + "m",
+ 0,
+ g.getHeight() - 49
+ );
+
+ let forward_eta = compute_eta(
+ now.getHours(),
+ now.getMinutes(),
+ approximate_speed,
+ remaining_distance / 1000
+ );
+
+ // now display ETA
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .setColor(g.theme.fg)
+ .drawString(forward_eta, 0, 42);
+
+ // display distance on path
+ g.setFont("6x8:2").drawString(
+ "" + rounded_distance + "/" + total,
+ 0,
+ g.getHeight() - 32
+ );
+
+ // display various indicators
+ if (this.distance_to_next_point <= 100) {
+ if (this.path.is_waypoint(this.reaching)) {
+ g.setColor(0.0, 1.0, 0.0)
+ .setFont("6x15")
+ .drawString("turn", g.getWidth() - 50, 30);
+ }
+ }
+ if (!this.on_path) {
+ g.setColor(1.0, 0.0, 0.0)
+ .setFont("6x15")
+ .drawString("lost", g.getWidth() - 55, 35);
+ }
+ }
+ display_path() {
+ // don't display all segments, only those neighbouring current segment
+ // this is most likely to be the correct display
+ // while lowering the cost a lot
+ //
+ // note that all code is inlined here to speed things up
+ let cos = this.adjusted_cos_direction;
+ let sin = this.adjusted_sin_direction;
+ let displayed_x = this.displayed_position.lon;
+ let displayed_y = this.displayed_position.lat;
+ let width = g.getWidth();
+ let height = g.getHeight();
+ let half_width = width / 2;
+ let half_height = height / 2 + Y_OFFSET;
+ let scale_factor = this.scale_factor;
+ if (!zoomed) {
+ scale_factor /= 2;
+ }
+
+ if (this.path !== null) {
+ // compute coordinate for projection on path
+ let tx = (this.projected_point.lon - displayed_x) * scale_factor;
+ let ty = (this.projected_point.lat - displayed_y) * scale_factor;
+ let rotated_x = tx * cos - ty * sin;
+ let rotated_y = tx * sin + ty * cos;
+ let projected_x = half_width - Math.round(rotated_x); // x is inverted
+ let projected_y = half_height + Math.round(rotated_y);
+
+ // display direction to next point if lost
+ if (!this.on_path) {
+ let next_point = this.path.point(this.current_segment + 1);
+ let previous_point = this.path.point(this.current_segment);
+ let nearest_point;
+ if (
+ previous_point.fake_distance(this.position) <
+ next_point.fake_distance(this.position)
+ ) {
+ nearest_point = previous_point;
+ } else {
+ nearest_point = next_point;
+ }
+ let tx = (nearest_point.lon - displayed_x) * scale_factor;
+ let ty = (nearest_point.lat - displayed_y) * scale_factor;
+ let rotated_x = tx * cos - ty * sin;
+ let rotated_y = tx * sin + ty * cos;
+ let x = half_width - Math.round(rotated_x); // x is inverted
+ let y = half_height + Math.round(rotated_y);
+ g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y);
+ }
+
+ // display current-segment's projection
+ g.setColor(0, 0, 0);
+ g.fillCircle(projected_x, projected_y, 4);
+ }
+
+ // now display ourselves
+ g.setColor(0, 0, 0);
+ g.fillCircle(half_width, half_height, 5);
+ }
}
function load_gps(filename) {
- // let's display splash screen while loading file
- g.clear();
- g.drawImage(splashscreen, 0, 0);
- g.setFont("6x8:2")
- .setFontAlign(-1, -1, 0)
- .setColor(0xf800)
- .drawString(filename, 0, g.getHeight() - 30);
- g.flip();
+ // let's display splash screen while loading file
- let buffer = s.readArrayBuffer(filename);
- let file_size = buffer.length;
- let offset = 0;
+ let splashscreen = require("heatshrink").decompress(
+ atob(
+ "2Gwgdly1ZATttAQfZARm2AQXbAREsyXJARmyAQXLAViDgARm2AQVbAR0kyVJAQ2yAQVLARZfBAQSD/ARXZAQVtARnbAQe27aAE5ICClgCMLgICCQEQCCkqDnARb+BAQW2AQyDEARdLAQeyAR3LAQSDXL51v+x9bfAICC7ICM23ZPpD4BAQXJn//7IFCAQ2yAQR6YQZOSQZpBBsiDZARm2AQVbAQSDIAQt///btufTAOyBYL+DARJrBAQSDWLJvvQYNlz/7tiAeEYICBtoCHQZ/+7ds//7tu2pMsyXJlmOnAFDyRoBAQSAWAQUlyVZAQxcBAQX//3ZsjIBWYUtBYN8uPHjqMeAQVbQZ/2QYXbQYNbQwRNBnHjyVLkhNBARvLAQSDLIgNJKZf/+1ZsjIBlmzQwXPjlwg8cux9YtoCD7ICCQZ192yDBIINt2f7tuSvED/0AgeOhMsyXJAQeyAQR6MARElyT+BAQ9lIIL+CsqDF21Ajlx4EAuPBQa4CIQZ0EQYNnAQNt2QCByU48f+nEAh05kuyC4L+DARJ3BAQSDJsmWpICEfwJQEkESoNl2wXByaDB2PAQYPHgEB4cgEYKDc7KDOkmAgMkyCABy3bsuegHjx/4QYM4sk27d/+XJlmSAQpcBAQSAKAQQ1BZAVZkoCHBYNIgEApMgEwcHQYUcgPHEYVv+SDaGQSDNAQZDByUbDQM48eOn/ggCDB23bIIICB/1LC4ICB2QCLPoICEfwNJARA1BAQZEDgEJkkyQAKDB/gCBQYUt+ACB/yDsAQVA8ESrKDC//+nIjB7dt/0bQYNJlmS5ICG2QCCcwQCGGQslAQdZAQ4RDQAPJQYUf//DGQKAB31LQYKeCQbmT//8QZlIQAM4QYkZQYe+raDCC4eyAQVLARaDBAoL4CAQNkz///4FCAQxWCp8AQAKDCjlwU4OCQYcv3yDfIAP/+SDM8EOQYOPCgOAhFl2CDB20bQwIUCfwICMLgICC2XLGQsnIISnDKAVZkoCDpKADAQUSoARBhcs2/Dlm2QbEEiFJggvBeAIAC5KDKpKDF8AIBgEAhMkw3LQYgCIfYICC2QCHCgl/IIf5smWpICIniDELgQdBoEAgVJkqDboMkiVBIAYABQZcjxyDB//4Bw2QRAIIEfAICC5ICM2XJkGSUgIXBIIvkEwklAQdZkiDD4IOBrILDC4UAQbYCBo5BF/iDKkiDB//+LgYCY2QCCpYCCkGCpEkwVPIIv/fwMkAQNkAQuRQYNwBAVZAQRoCRgSDcv5BG+RlLvHjQDHJAQUsAQ6DBhACBn5BG/wpOrMlARZuBAQSDRgEQgMAiJAGAAPJgmQpMEfbQCSpaDDx5BJCgVkAQWWARhoBAQR9SQY0AoEEv5BI/MkiVBPs0sAQfJAQUAQYQ5Bj4CB/hHEExz+BAQT+BARVlAQSDPAAKDJ/8EiFBAQeQQ0gCFkECgEj//HQYUcuPHIIXkwQaHfYICCsgCMrICCQByDFHwQAI/iDFiVBkkSQc3JIIfx46ACAQ1yhEgyUJAQImOrICCkoCLPQICCQZCCKAAXBQYYCFyFJgiGiIIX8QBACD4EgwVIkmCDo1kAQWWARh0BAQR9GQY8H8aDM/CDJiVBkkSQccHQBQCDgGChCGBAQOShImLfYICFfwICKsoCCQYcAQRn+n/8iEBgCGIAQWQQbtPQaMcuSDEwVIkmCEw77BAQVkARlZAQSACAQN/IIM/8f+nCCI8f//H/x0AgkAoCDJiVBkkSQbOT/8AgKANAQiDEAQsJkA1PrICCkoCIz5BBhyDBxyDJAAYOB/iZBAAMBgCGIAQdJgiDUFwKDUjkCQZEIkmCpApCsgCFywCLv9lAoNl//HQYk/P5Hjx4GE+CEDgkAoCDKoMkiQCBPpeT//8AoMnQYSARAQVwH4OAQxMgyUJAQQ7IfwICCrMlz48B+VZngsBgeP/CAIAAaDB8YGD/CEDAAMDMQUQgKJJyFJAQRKGEYK8BhIqCQCQCEgECgEggUIEAX8QwkkwVIHAz7BAQVkAQN/+KqCg4pCOIKDN/0/QwQADwCCCBYIRDoEEgCDHAQMkiQCBJQiABnHggE4VoSDXAQPAgEPKoyDCAQkJkCGFAQdPEYcBFIaAMABsDBA/8gEBgEQgKGIAQNJgmSnCDDhwFDQbICBv5MI5CGFkmCpCACsgCCyImJfAYAOCIPjBA4TI8kAoCDKoMnPQJ9CgeAAQKDdAQMfHgXxBYl+QYYCEhMgyUJngRBgAAHf6R6Cx4FCnALDxyGC/BuCAQVAFoUQgKDEoARF8EOgACBiSDdjlwg4LIpMkhSGHo8cQJEkyRuDABxcBQwaDBMoIFCEYMONwY+BnFL12SoEgoEEgCDCCIfjwE4gYCBhMk2SDeuPAIQKGDFIOSIgICCyCDDwPAQY8SCgXjQaL4FAowAB+EAgYIB9cu3Xrlmy5JECGwIOCDQYCC0gOBCgKAbuB9DAQUAgPHQAgCEkUHP4wABTAplDABaSDPogCDEgMOQwX6r/+QYJrB5csySDCpaAIx06pYUEQbUAAQQABBAPSpF145uFAQOXjkB4ACCC4VIgCVGQYf+n7+FAgYLFMonghyrEh0SpeuyVIkmypEgF4MuQBE49IRB9euQYWyQbUcdw0HNYoCCpFwg8AAQYVDSo6DDKAKDLnAFF8EAfYOAgHj1gjBRIPjlxrDGQOQQBACBnVLl269esQbhrBhMh4BoEw8dNwslDQvAjkBAQKAHQYn4QZHjx4EBL4IJCMokA9ck3ED1xoBlmS8LyB5MgRgSAIAQOkPoIaD2VLlmCQbF0L4ZrLrgUBgCYBAQYABTYgCGPQwAELgX//xfBAQRlCxmS9euyTsCdISABAQKPBQBOOnVJCgKDCC4cgQbEAMpQCDkoaHgPAjkEDRj4C8aGCQY4CGwm48EEMoOscwQFBAQNIkApBhyAInCABTwSbB1waCAoMk2SDVuj1BAQJoLrgXFuEHgFwgUJTxpWDfASADn5iFgYCBgEO2XpLgPL0mSMQOSF4UIkmQTxOOiCYCQYIdBAQUuQYILBPprjBAoMAAQUAMplJkojKuAaNQYoCCQY47BnHgeQPggG69aDENwOChEgwUJCIKDKTAKDCAQKDC5Ms3XIkCDFPQYCE4VcIQIABi8cMptIU5UADRqDHgHj/xiG9JBDiXj0hlB1hrB0mCEAKABkmQDQihDAQQyCPQOyTYIdB1iGBBANIAQMcgLaCgBiIKwtdMpmHDpApBQB4CCeoXhh0QQY+Q9ek3Xr1z+BcYLsDQYKABEYIgBDQYgE9eOiQXCAQI4DQwIIBkmyhYLBgBZBjpZBL4clMQhlQpCAIAQMJQacAgiDBl26L4M6fYO4AoJ3BxgCB126pekL4fJkGChEgyT+FAQvpF4PJOgKDBwR6BUgYCCBwOygB6BVQR9BgVckmXjkAMSIUBQZPSQCKDDl04eoKDDoeu3DmBfYRZBSQLpCQYIdBQYJcBPomP/AFDwm4fYXJkmCpACBHAOy5CPCBAMJCIMJkPCI4VcuESeQcBMqCAJAQNwQCQCCheunT4CoeAiXr1m69MAmSDDcAlLL4MIkGSpb+E8f+AoihBVoXLCgL7C9csDodJAoMLQYZ3DrkAKAkgRIYCLQBICCuiDWPQKDCcYL4BBAaJCBAMsLgWShKDCkmQPQgCG8L7B5aDDAoaDBTwKJC1ytDI4tIL4qPEARMlQBVxDRoCKbQXol2y9JxBpaDBKASJB2TmBQAkgwVJhx9Ex/4QYkQDoVLF4IjFQAXIkizCFgSDGASlcQBICBuAmYpcuJQICCcYRZBL4YIB5MgQYKABQYOSfwvj/wFD8MAPoIgEhICB5L4FQYQRBRIKDaw6AJAQMBVTLRCJQSDCAoTpDPoKDCQAOCDQKAEAQ8LlhxCyRxChCnCliPB1wOBEYI7C5ACBQbCAKjdtwCqZQYZTDAoSDBBYtJLgKDBC4J9F//4AoXbtuwpcuOgIdBfYL4DEwOS9aDBFIOC5ckAQMuQbCAIAQPG7VtmiDbkGy5IFB5KGDAQYIChKDCkm4fwv/Aoc27dp01L0gmCwXr1gjDDoIFB1ytBBwIRCBARZVkqAIAQX2YoMwQbbdB5L1BhJZBboR9BAoSABQYNJhyADAQ2P2xBBw9LPoNIC4KDBOIIvB5B6CAoICBEwIFB9aDWriAJAQRBCnCDgbQJQCwUJlzdCBYWQPov//yDFYoXHof8EwRxBFgJ3CEYOC5KwBQYVLl26SoZWSw6AKAQMB/5KCjsEQbICBLgO65JWBhJWBpbUEd4J6Ex0//6JEoel4BCB48IDoPrkiGBAQa2CWASDBBAQvBSoZWRQBYCBpMF/8DI4NAQCyDEwT4BZwJTBBYJQBl2ShIOBhZ6EfwP/RIk68eBQQKDBgKDCeoPIFgYpBBYIFCQYXLQAPr1iDSQBYCB6VIurFB/04pf0QbFJkGChMsQYOucwRTCBwW4PQgCB//4BAkQYoUcv/CpMMEAOu3QgBwVIF4QpCAoPJAoICB2SGCKB8lQBaDDKYOS/+kWwaDZJQLOCcYLRByVLcAUOQAmPQAoCCEAME3UJZANBDQPJlxxD5AvBQZFIQadIQBgCBF4NIkrCBkkSQDCDE5ZKB9YCBRIJcBLIMDPQv/QY+uPQMEiVBgmyhBrCAQIpBU4R0DPQOCBwY7BBwIIBKBqAMkoCBCgeQpApBQb5oBAQSDBhEg3B6F//+QAmEyCDBTYWyfAL+BFIQgBF4SDCQAIFE126QYQUBQZp0CQZd0y4UCpB9aAQihCKYSJCFIOChEuPQmOn//RIiDB3VJlz+CTYRxBJRCDF1g1B1myRIOCTwKDMpCALQYYUEQcACBdISDBwSMBwVDPQuP/6JEQYfrdgIjC5CDD2QFBF4Wy5ICDQYOu2XrQYKPBQYI1BJpaAMAQVwQchWCAoZKBdgO4PQwCJPQMu3RxCPoyqB5YCCFgeyQYKeBBYNIQZ0lQBoCCuiDkLIRlCJQUIhyAOnHpDoRuBfAZoCQAosEpAUBBAKDB1iDBBYNLkiDJpCAOAQMJPr4CFJoLXCyUIMoMDQBoCB3FL1gdBNwPrEYSGCQAQFDBYaDDAoKPCQYcsQZKAOjskw6AjAQREBQYuAPQ3//AIFoeu3VLAQSDCRIQmB9ekFgSDBGQe6PQKABGQIOCAQQ+DJQ2HQZvXQEwCDIgMJkGCQYL+G//+BAs6QAL1C3TvDQYJoCRIOCpYsBhYIBpEuCga2BfwdLBYUsRIRHEkKALAQXCrqDuhaAEAQM//4IGQYW6QYKABQYQFBQYXLSQMLkgmBBAMIO4UgGoICCQYQjBQZFcQBgCDQE4CBhJWCQYJ3EAQOP/4IGAQKbBL4RlBeQQCCQYR6B9esR4fIBANLQAeCDQOShaDJy6AOQY+CMQaDgAQKDB3CDQiXJO4PJEARiBQwQICNYKDDpYOBC4IRDBAIRCQYYaBQYklQB6DFpCDBQAazDATcIEwICBfY3j//4QY86MQSDDfwREDwXLNYPrPoQUBQASPD1wLDQZMhQaEgwCDEMoiDfpBfBhMOQY3//yMHeQIdDdgZuBPQILBwRrCQwQCB3SDCpcuBAJ9BDQKGCAQJEFQBwCBjt0PRkJQbkIQYMDfYwCJ8JcBcAaDBQARrCQYYICQYnrTwPLQYKGBTYYaCCIOCIgSAOQYbdDQdSAO8eunFBPoKDByTmBQYOkRgIFBEwSDC5MgBYR6B1x3BAQQIBQAXIEASDDy6DPkmHpAXDTwZlGQb24QZ+kyFLOgSDD2RiBPoYmCKYL1DBYSACpcufwQCBSQKDD1hoCw6DPkvXLgiDpPQ3//yDIdgJcBfwVL0h3CyRuCFIiDDAQSYCUIJ9BCIMLQYwaBkqANAQV16S2EMQqJDBY6DWlx6Fn//QAoCCwkyQYJ3BlxfB0iACQZCVDfwYFBpJ9CBwMJRIQRC1gdBQBwCCuAvDO4cgQYgFBQbsLO4uP/6AGAQPhhxWBQYe6QAXJEw4LDOIRNBQYXIQYMIQYYIBBYNLFINIQaEJQYIdCHAaDCAQqDcgZ6F/6DJpYyCLgPrkm6EAiMBQY5TGfwSDB5AOEboaDBQByDDkESQYogCEYYCfO4qCB/CDI8ckiVLC4KDBPoQCBMQPr0gLB1jvCFgcIkGCKYOy5YLBQYQUCQa3CQASDIQECDHn///yAHx069ZWBOIXL1zyDBYO65esAoICBhIUBNwKDCQAKDEDQYgDQbB6jQZ6AGQYfBQYZoBl265JuCkm6PQQFBwUIBYPJBAKJC5MgBwKDCRgKDBSoWCCISDQ6VBL5AsBAoVIQceP/6DKiR6CO4QaBQYQjGQYRHBPoILDQYWCRgVIQYNL126RgOyeQOCQZ50EC4OSWwImCQwaDkQQKAHAQOEEaR9BQYTRGKwOCpaDBhCDBR4SDCBwSDPuAmCwSDCAQQ1DQwSDiQQKDKx0SFjSDFBASDCcwQRDBwIA="
+ )
+ );
- let path = null;
- let maps = [];
- let interests = null;
- while (offset < file_size) {
- let block_type = Uint8Array(buffer, offset, 1)[0];
- offset += 1;
- if (block_type == 0) {
- // it's a map
- console.log("loading map");
- let res = new Map(buffer, offset, filename);
- let map = res[0];
- offset = res[1];
- maps.push(map);
- } else if (block_type == 2) {
- console.log("loading path");
- let res = new Path(buffer, offset);
- path = res[0];
- offset = res[1];
- } else if (block_type == 3) {
- console.log("loading interests");
- let res = new Interests(buffer, offset);
- interests = res[0];
- offset = res[1];
- } else {
- console.log("todo : block type", block_type);
+ g.clear();
+
+ g.drawImage(splashscreen, 0, 0);
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .setColor(0xf800)
+ .drawString(filename, 0, g.getHeight() - 30);
+ g.flip();
+
+ let buffer = s.readArrayBuffer(filename);
+ let file_size = buffer.length;
+ let offset = 0;
+
+ let path = null;
+ let maps = [];
+ let interests = null;
+ while (offset < file_size) {
+ let block_type = Uint8Array(buffer, offset, 1)[0];
+ offset += 1;
+ if (block_type == 0) {
+ // it's a map
+ console.log("loading map");
+ let res = new Map(buffer, offset, filename);
+ let map = res[0];
+ offset = res[1];
+ maps.push(map);
+ } else if (block_type == 2) {
+ console.log("loading path");
+ let res = new Path(buffer, offset);
+ path = res[0];
+ offset = res[1];
+ } else if (block_type == 3) {
+ console.log("loading interests");
+ let res = new Interests(buffer, offset);
+ interests = res[0];
+ offset = res[1];
+ } else {
+ console.log("todo : block type", block_type);
+ }
}
- }
- // checksum file size
- if (offset != file_size) {
- console.log("invalid file size", file_size, "expected", offset);
- let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset;
- E.showAlert(msg).then(function () {
- E.showAlert();
- start_gipy(path, maps, interests);
- });
- } else {
- start_gipy(path, maps, interests);
- }
+ // checksum file size
+ if (offset != file_size) {
+ console.log("invalid file size", file_size, "expected", offset);
+ let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset;
+ E.showAlert(msg).then(function() {
+ E.showAlert();
+ start_gipy(path, maps, interests);
+ });
+ } else {
+ start_gipy(path, maps, interests);
+ }
}
class Path {
- constructor(buffer, offset) {
- // let p = Uint16Array(buffer, offset, 1);
- // console.log(p);
- let points_number = Uint16Array(buffer, offset, 1)[0];
- offset += 2;
+ constructor(buffer, offset) {
+ // let p = Uint16Array(buffer, offset, 1);
+ // console.log(p);
+ let points_number = Uint16Array(buffer, offset, 1)[0];
+ offset += 2;
- // path points
- this.points = Float64Array(buffer, offset, points_number * 2);
- offset += 8 * points_number * 2;
+ // path points
+ this.points = Float64Array(buffer, offset, points_number * 2);
+ offset += 8 * points_number * 2;
- // path waypoints
- let waypoints_len = Math.ceil(points_number / 8.0);
- this.waypoints = Uint8Array(buffer, offset, waypoints_len);
- offset += waypoints_len;
+ // path waypoints
+ let waypoints_len = Math.ceil(points_number / 8.0);
+ this.waypoints = Uint8Array(buffer, offset, waypoints_len);
+ offset += waypoints_len;
- return [this, offset];
- }
-
- is_waypoint(point_index) {
- let i = Math.floor(point_index / 8);
- let subindex = point_index % 8;
- let r = this.waypoints[i] & (1 << subindex);
- return r != 0;
- }
-
- // return point at given index
- point(index) {
- let lon = this.points[2 * index];
- let lat = this.points[2 * index + 1];
- return new Point(lon, lat);
- }
-
- // return index of segment which is nearest from point.
- // we need a direction because we need there is an ambiguity
- // for overlapping segments which are taken once to go and once to come back.
- // (in the other direction).
- nearest_segment(point, start, end, cos_direction, sin_direction) {
- // we are going to compute two min distances, one for each direction.
- let indices = [0, 0];
- let mins = [Number.MAX_VALUE, Number.MAX_VALUE];
-
- let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]);
- for (let i = start + 1; i < end + 1; i++) {
- let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]);
-
- let closest_point = point.closest_segment_point(p1, p2);
- let distance = point.length_squared(closest_point);
-
- let dot =
- cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat);
- let orientation = +(dot < 0); // index 0 is good orientation
- if (distance <= mins[orientation]) {
- mins[orientation] = distance;
- indices[orientation] = i - 1;
- }
-
- p1 = p2;
+ return [this, offset];
}
- // by default correct orientation (0) wins
- // but if other one is really closer, return other one
- if (mins[1] < mins[0] / 100.0) {
- return [1, indices[1]];
- } else {
- return [0, indices[0]];
+ is_waypoint(point_index) {
+ let i = Math.floor(point_index / 8);
+ let subindex = point_index % 8;
+ let r = this.waypoints[i] & (1 << subindex);
+ return r != 0;
+ }
+
+ // return point at given index
+ point(index) {
+ let lon = this.points[2 * index];
+ let lat = this.points[2 * index + 1];
+ return new Point(lon, lat);
+ }
+
+ // return index of segment which is nearest from point.
+ // we need a direction because we need there is an ambiguity
+ // for overlapping segments which are taken once to go and once to come back.
+ // (in the other direction).
+ nearest_segment(point, start, end, cos_direction, sin_direction) {
+ // we are going to compute two min distances, one for each direction.
+ let indices = [0, 0];
+ let mins = [Number.MAX_VALUE, Number.MAX_VALUE];
+
+ let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]);
+ for (let i = start + 1; i < end + 1; i++) {
+ let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]);
+
+ let closest_point = point.closest_segment_point(p1, p2);
+ let distance = point.length_squared(closest_point);
+
+ let dot =
+ cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat);
+ let orientation = +(dot < 0); // index 0 is good orientation
+ if (distance <= mins[orientation]) {
+ mins[orientation] = distance;
+ indices[orientation] = i - 1;
+ }
+
+ p1 = p2;
+ }
+
+ // by default correct orientation (0) wins
+ // but if other one is really closer, return other one
+ if (mins[1] < mins[0] / 100.0) {
+ return [1, indices[1]];
+ } else {
+ return [0, indices[0]];
+ }
+ }
+ get len() {
+ return this.points.length / 2;
}
- }
- get len() {
- return this.points.length / 2;
- }
}
class Point {
- constructor(lon, lat) {
- this.lon = lon;
- this.lat = lat;
- }
- coordinates(current_position, cos_direction, sin_direction, scale_factor) {
- let translated = this.minus(current_position).times(scale_factor);
- let rotated_x =
- translated.lon * cos_direction - translated.lat * sin_direction;
- let rotated_y =
- translated.lon * sin_direction + translated.lat * cos_direction;
- return [
- g.getWidth() / 2 - Math.round(rotated_x), // x is inverted
- g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET,
- ];
- }
- minus(other_point) {
- let xdiff = this.lon - other_point.lon;
- let ydiff = this.lat - other_point.lat;
- return new Point(xdiff, ydiff);
- }
- plus(other_point) {
- return new Point(this.lon + other_point.lon, this.lat + other_point.lat);
- }
- length_squared(other_point) {
- let londiff = this.lon - other_point.lon;
- let latdiff = this.lat - other_point.lat;
- return londiff * londiff + latdiff * latdiff;
- }
- times(scalar) {
- return new Point(this.lon * scalar, this.lat * scalar);
- }
- dot(other_point) {
- return this.lon * other_point.lon + this.lat * other_point.lat;
- }
- distance(other_point) {
- //see https://www.movable-type.co.uk/scripts/latlong.html
- const R = 6371e3; // metres
- const phi1 = (this.lat * Math.PI) / 180;
- const phi2 = (other_point.lat * Math.PI) / 180;
- const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180;
- const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180;
-
- const a =
- Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) +
- Math.cos(phi1) *
- Math.cos(phi2) *
- Math.sin(deltalambda / 2) *
- Math.sin(deltalambda / 2);
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
-
- return R * c; // in meters
- }
- fake_distance(other_point) {
- return Math.sqrt(this.length_squared(other_point));
- }
- // return closest point from 'this' on [v,w] segment.
- // since this function is critical we inline all code here.
- closest_segment_point(v, w) {
- // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
- // Return minimum distance between line segment vw and point p
- let segment_londiff = w.lon - v.lon;
- let segment_latdiff = w.lat - v.lat;
- let l2 =
- segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt
- if (l2 == 0.0) {
- return v; // v == w case
+ constructor(lon, lat) {
+ this.lon = lon;
+ this.lat = lat;
}
- // Consider the line extending the segment, parameterized as v + t (w - v).
- // We find projection of point p onto the line.
- // It falls where t = [(p-v) . (w-v)] / |w-v|^2
- // We clamp t from [0,1] to handle points outside the segment vw.
-
- // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below
- let start_londiff = this.lon - v.lon;
- let start_latdiff = this.lat - v.lat;
- let t =
- (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2;
- if (t < 0) {
- t = 0;
- } else {
- if (t > 1) {
- t = 1;
- }
+ coordinates(current_position, cos_direction, sin_direction, scale_factor) {
+ let translated = this.minus(current_position).times(scale_factor);
+ let rotated_x =
+ translated.lon * cos_direction - translated.lat * sin_direction;
+ let rotated_y =
+ translated.lon * sin_direction + translated.lat * cos_direction;
+ return [
+ g.getWidth() / 2 - Math.round(rotated_x), // x is inverted
+ g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET,
+ ];
+ }
+ minus(other_point) {
+ let xdiff = this.lon - other_point.lon;
+ let ydiff = this.lat - other_point.lat;
+ return new Point(xdiff, ydiff);
+ }
+ plus(other_point) {
+ return new Point(this.lon + other_point.lon, this.lat + other_point.lat);
+ }
+ length_squared(other_point) {
+ let londiff = this.lon - other_point.lon;
+ let latdiff = this.lat - other_point.lat;
+ return londiff * londiff + latdiff * latdiff;
+ }
+ times(scalar) {
+ return new Point(this.lon * scalar, this.lat * scalar);
+ }
+ // dot(other_point) {
+ // return this.lon * other_point.lon + this.lat * other_point.lat;
+ // }
+ distance(other_point) {
+ //see https://www.movable-type.co.uk/scripts/latlong.html
+ const R = 6371e3; // metres
+ const phi1 = (this.lat * Math.PI) / 180;
+ const phi2 = (other_point.lat * Math.PI) / 180;
+ const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180;
+ const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180;
+
+ const a =
+ Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) +
+ Math.cos(phi1) *
+ Math.cos(phi2) *
+ Math.sin(deltalambda / 2) *
+ Math.sin(deltalambda / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return R * c; // in meters
+ }
+ fake_distance(other_point) {
+ return Math.sqrt(this.length_squared(other_point));
+ }
+ // return closest point from 'this' on [v,w] segment.
+ // since this function is critical we inline all code here.
+ closest_segment_point(v, w) {
+ // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
+ // Return minimum distance between line segment vw and point p
+ let segment_londiff = w.lon - v.lon;
+ let segment_latdiff = w.lat - v.lat;
+ let l2 =
+ segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt
+ if (l2 == 0.0) {
+ return v; // v == w case
+ }
+ // Consider the line extending the segment, parameterized as v + t (w - v).
+ // We find projection of point p onto the line.
+ // It falls where t = [(p-v) . (w-v)] / |w-v|^2
+ // We clamp t from [0,1] to handle points outside the segment vw.
+
+ // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below
+ let start_londiff = this.lon - v.lon;
+ let start_latdiff = this.lat - v.lat;
+ let t =
+ (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2;
+ if (t < 0) {
+ t = 0;
+ } else {
+ if (t > 1) {
+ t = 1;
+ }
+ }
+ let lon = v.lon + segment_londiff * t;
+ let lat = v.lat + segment_latdiff * t;
+ return new Point(lon, lat);
}
- let lon = v.lon + segment_londiff * t;
- let lat = v.lat + segment_latdiff * t;
- return new Point(lon, lat);
- }
}
let fake_gps_point = 0;
-function simulate_gps(status) {
- if (status.path === null) {
- let map = status.maps[0];
- let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]);
- let p2 = new Point(
- map.start_coordinates[0] + map.side * map.grid_size[0],
- map.start_coordinates[1] + map.side * map.grid_size[1]
- );
- let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point));
- if (fake_gps_point < 1) {
- fake_gps_point += 0.01;
- }
- status.update_position(pos, null, null);
- } else {
- if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) {
- return;
- }
- let point_index = Math.floor(fake_gps_point);
- if (point_index >= status.path.len / 2 - 1) {
- return;
- }
- let p1 = status.path.point(2 * point_index); // use these to approximately follow path
- let p2 = status.path.point(2 * (point_index + 1));
- //let p1 = status.path.point(point_index); // use these to strictly follow path
- //let p2 = status.path.point(point_index + 1);
- let alpha = fake_gps_point - point_index;
- let pos = p1.times(1 - alpha).plus(p2.times(alpha));
-
- if (go_backwards) {
- fake_gps_point -= 0.05; // advance simulation
- } else {
- fake_gps_point += 0.05; // advance simulation
- }
- status.update_position(pos, null, null);
- }
-}
function drawMenu() {
- const menu = {
- "": { title: "choose trace" },
- };
- var files = s.list(".gps");
- for (var i = 0; i < files.length; ++i) {
- menu[files[i]] = start.bind(null, files[i]);
- }
- menu["Exit"] = function () {
- load();
- };
- E.showMenu(menu);
+ const menu = {
+ "": {
+ title: "choose trace"
+ },
+ };
+ var files = s.list(".gps");
+ for (var i = 0; i < files.length; ++i) {
+ menu[files[i]] = start.bind(null, files[i]);
+ }
+ menu["Exit"] = function() {
+ load();
+ };
+ E.showMenu(menu);
}
function start(fn) {
- E.showMenu();
- console.log("loading", fn);
+ E.showMenu();
+ console.log("loading", fn);
- load_gps(fn);
+ load_gps(fn);
}
function start_gipy(path, maps, interests) {
- console.log("starting");
- status = new Status(path, maps, interests);
+ console.log("starting");
- if (status.path !== null) {
- let start = status.path.point(0);
- status.displayed_position = start;
- } else {
- let first_map = maps[0];
- status.displayed_position = new Point(
- first_map.start_coordinates[0] +
- (first_map.side * first_map.grid_size[0]) / 2,
- first_map.start_coordinates[1] +
- (first_map.side * first_map.grid_size[1]) / 2);
- }
- status.display();
-
- Bangle.on("stroke", (o) => {
- if (in_menu) {
- return;
+ if (!simulated && settings.disable_bluetooth) {
+ NRF.sleep(); // disable bluetooth completely
}
- // we move display according to stroke
- let first_x = o.xy[0];
- let first_y = o.xy[1];
- let last_x = o.xy[o.xy.length - 2];
- let last_y = o.xy[o.xy.length - 1];
- let xdiff = last_x - first_x;
- let ydiff = last_y - first_y;
- let c = status.adjusted_cos_direction;
- let s = status.adjusted_sin_direction;
- let rotated_x = xdiff * c - ydiff * s;
- let rotated_y = xdiff * s + ydiff * c;
- status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor;
- status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor;
+ status = new Status(path, maps, interests);
+
+ setWatch(
+ function() {
+ status.activate();
+ if (in_menu) {
+ return;
+ }
+ in_menu = true;
+ const menu = {
+ "": {
+ title: "choose action"
+ },
+ "Go Backward": {
+ value: go_backwards,
+ format: (v) => (v ? "On" : "Off"),
+ onchange: (v) => {
+ go_backwards = v;
+ },
+ },
+ Zoom: {
+ value: zoomed,
+ format: (v) => (v ? "In" : "Out"),
+ onchange: (v) => {
+ status.invalidate_caches();
+ zoomed = v;
+ },
+ },
+ /*LANG*/
+ "powersaving": {
+ value: powersaving,
+ onchange: (v) => {
+ powersaving = v;
+ }
+ },
+ "back to map": function() {
+ in_menu = false;
+ E.showMenu();
+ g.clear();
+ g.flip();
+ if (status !== null) {
+ status.display();
+ }
+ },
+ };
+ E.showMenu(menu);
+ },
+ BTN1, {
+ repeat: true
+ }
+ );
+
+
+ if (status.path !== null) {
+ let start = status.path.point(0);
+ status.displayed_position = start;
+ } else {
+ let first_map = maps[0];
+ status.displayed_position = new Point(
+ first_map.start_coordinates[0] +
+ (first_map.side * first_map.grid_size[0]) / 2,
+ first_map.start_coordinates[1] +
+ (first_map.side * first_map.grid_size[1]) / 2);
+ }
status.display();
- });
- if (simulated) {
- status.starting_time = getTime();
- // let's keep the screen on in simulations
- Bangle.setLCDTimeout(0);
- Bangle.setLCDPower(1);
- setInterval(simulate_gps, 500, status);
- } else {
- Bangle.setLocked(false);
-
- let frame = 0;
- let set_coordinates = function (data) {
- frame += 1;
- // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere
- let valid_coordinates =
- !isNaN(data.lat) &&
- !isNaN(data.lon) &&
- (data.lat != 0.0 || data.lon != 0.0);
- if (valid_coordinates) {
- if (status.starting_time === null) {
- status.starting_time = getTime();
- Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen
+ Bangle.on("stroke", (o) => {
+ status.activate();
+ if (in_menu) {
+ return;
}
- status.update_position(new Point(data.lon, data.lat), null, data.time);
- }
- let gps_status_color;
- if (frame % 2 == 0 || valid_coordinates) {
- gps_status_color = g.theme.bg;
- } else {
- gps_status_color = g.theme.fg;
- }
- if (!in_menu) {
- g.setColor(gps_status_color)
- .setFont("6x8:2")
- .drawString("gps", g.getWidth() - 40, 30);
- }
- };
+ // we move display according to stroke
+ let first_x = o.xy[0];
+ let first_y = o.xy[1];
+ let last_x = o.xy[o.xy.length - 2];
+ let last_y = o.xy[o.xy.length - 1];
+ let xdiff = last_x - first_x;
+ let ydiff = last_y - first_y;
- Bangle.setGPSPower(true, "gipy");
- Bangle.on("GPS", set_coordinates);
- Bangle.on("lock", function (on) {
- if (!on) {
- Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking
- }
+ let c = status.adjusted_cos_direction;
+ let s = status.adjusted_sin_direction;
+ let rotated_x = xdiff * c - ydiff * s;
+ let rotated_y = xdiff * s + ydiff * c;
+ status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor;
+ status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor;
+ status.display();
});
- }
-}
-setWatch(
- function () {
- if (in_menu) {
- return;
- }
- in_menu = true;
- const menu = {
- "": { title: "choose action" },
- "Go Backward": {
- value: go_backwards,
- format: (v) => (v ? "On" : "Off"),
- onchange: (v) => {
- go_backwards = v;
- },
- },
- Zoom: {
- value: zoomed,
- format: (v) => (v ? "In" : "Out"),
- onchange: (v) => {
- status.invalidate_caches();
- zoomed = v;
- },
- },
- "back to map": function () {
- in_menu = false;
- E.showMenu();
- g.clear();
- g.flip();
- if (status !== null) {
- status.display();
+ if (simulated) {
+ status.starting_time = getTime();
+ // let's keep the screen on in simulations
+ Bangle.setLCDTimeout(0);
+ Bangle.setLCDPower(1);
+ Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen
+
+
+ function simulate_gps(status) {
+ if (status.path === null) {
+ let map = status.maps[0];
+ let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]);
+ let p2 = new Point(
+ map.start_coordinates[0] + map.side * map.grid_size[0],
+ map.start_coordinates[1] + map.side * map.grid_size[1]
+ );
+ let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point));
+ if (fake_gps_point < 1) {
+ fake_gps_point += 0.05;
+ }
+ status.update_position(pos);
+ } else {
+ if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) {
+ return;
+ }
+ let point_index = Math.floor(fake_gps_point);
+ if (point_index >= status.path.len / 2 - 1) {
+ return;
+ }
+ let p1 = status.path.point(2 * point_index); // use these to approximately follow path
+ let p2 = status.path.point(2 * (point_index + 1));
+ //let p1 = status.path.point(point_index); // use these to strictly follow path
+ //let p2 = status.path.point(point_index + 1);
+
+ let alpha = fake_gps_point - point_index;
+ let pos = p1.times(1 - alpha).plus(p2.times(alpha));
+
+ if (go_backwards) {
+ fake_gps_point -= 0.05; // advance simulation
+ } else {
+ fake_gps_point += 0.05; // advance simulation
+ }
+ status.update_position(pos);
+ }
}
- },
- };
- E.showMenu(menu);
- },
- BTN1,
- { repeat: true }
-);
+
+ setInterval(simulate_gps, 500, status);
+ } else {
+ status.activate();
+
+ let frame = 0;
+ let set_coordinates = function(data) {
+ frame += 1;
+ // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere
+ let valid_coordinates = !isNaN(data.lat) &&
+ !isNaN(data.lon) &&
+ (data.lat != 0.0 || data.lon != 0.0);
+ if (valid_coordinates) {
+ if (status.starting_time === null) {
+ status.starting_time = getTime();
+ Bangle.loadWidgets(); // load them even in simulation to eat mem
+ }
+ status.update_position(new Point(data.lon, data.lat));
+ }
+ let gps_status_color;
+ if (frame % 2 == 0 || valid_coordinates) {
+ gps_status_color = g.theme.bg;
+ } else {
+ gps_status_color = g.theme.fg;
+ }
+ if (!in_menu) {
+ g.setColor(gps_status_color)
+ .setFont("6x8:2")
+ .drawString("gps", g.getWidth() - 40, 30);
+ }
+ };
+
+ Bangle.setGPSPower(true, "gipy");
+ Bangle.on("GPS", set_coordinates);
+ }
+}
let files = s.list(".gps");
if (files.length <= 1) {
- if (files.length == 0) {
- load();
- } else {
- start(files[0]);
- }
+ if (files.length == 0) {
+ load();
+ } else {
+ start(files[0]);
+ }
} else {
- drawMenu();
-}
+ drawMenu();
+}
\ No newline at end of file
diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json
index 8b8c88780..7dd4123f6 100644
--- a/apps/gipy/metadata.json
+++ b/apps/gipy/metadata.json
@@ -2,7 +2,7 @@
"id": "gipy",
"name": "Gipy",
"shortName": "Gipy",
- "version": "0.19",
+ "version": "0.20",
"description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.",
"allow_emulator":false,
"icon": "gipy.png",
diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts
index 15a90b1e8..c881052f4 100644
--- a/apps/gipy/pkg/gps.d.ts
+++ b/apps/gipy/pkg/gps.d.ts
@@ -67,11 +67,11 @@ export interface InitOutput {
readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table;
- readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a: (a: number, b: number, c: number) => void;
+ readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;
- readonly wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b: (a: number, b: number, c: number, d: number) => void;
+ readonly wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137: (a: number, b: number, c: number, d: number) => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js
index ce9ebe5f8..39c2a6804 100644
--- a/apps/gipy/pkg/gps.js
+++ b/apps/gipy/pkg/gps.js
@@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_24(arg0, arg1, arg2) {
- wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(arg0, arg1, addHeapObject(arg2));
+ wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(arg0, arg1, addHeapObject(arg2));
}
function _assertClass(instance, klass) {
@@ -369,7 +369,7 @@ function handleError(f, args) {
}
}
function __wbg_adapter_84(arg0, arg1, arg2, arg3) {
- wasm.wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
+ wasm.wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
}
/**
@@ -460,6 +460,21 @@ function getImports() {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
+ imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
+ const ret = getObject(arg0).signal;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
+ const ret = new AbortController();
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
+ getObject(arg0).abort();
+ };
+ imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
+ return addHeapObject(ret);
+ }, arguments) };
imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () {
const ret = new Headers();
return addHeapObject(ret);
@@ -496,21 +511,6 @@ function getImports() {
const ret = getObject(arg0).text();
return addHeapObject(ret);
}, arguments) };
- imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
- const ret = getObject(arg0).signal;
- return addHeapObject(ret);
- };
- imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
- const ret = new AbortController();
- return addHeapObject(ret);
- }, arguments) };
- imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
- getObject(arg0).abort();
- };
- imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
- const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
- return addHeapObject(ret);
- }, arguments) };
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error();
return addHeapObject(ret);
@@ -675,8 +675,8 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
- imports.wbg.__wbindgen_closure_wrapper2298 = function(arg0, arg1, arg2) {
- const ret = makeMutClosure(arg0, arg1, 260, __wbg_adapter_24);
+ imports.wbg.__wbindgen_closure_wrapper2245 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 267, __wbg_adapter_24);
return addHeapObject(ret);
};
diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm
index 6999cb946..8e0fbc07e 100644
Binary files a/apps/gipy/pkg/gps_bg.wasm and b/apps/gipy/pkg/gps_bg.wasm differ
diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts
index df9a024fa..b4303ee30 100644
--- a/apps/gipy/pkg/gps_bg.wasm.d.ts
+++ b/apps/gipy/pkg/gps_bg.wasm.d.ts
@@ -12,8 +12,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number): number;
export const __wbindgen_export_2: WebAssembly.Table;
-export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(a: number, b: number, c: number): void;
+export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_exn_store(a: number): void;
-export function wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(a: number, b: number, c: number, d: number): void;
+export function wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(a: number, b: number, c: number, d: number): void;
diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js
index 1f6ae0853..395b1ac93 100644
--- a/apps/gipy/settings.js
+++ b/apps/gipy/settings.js
@@ -1,29 +1,66 @@
-(function (back) {
- var FILE = "gipy.json";
- // Load settings
- var settings = Object.assign(
- {
- lost_distance: 50,
- },
- require("Storage").readJSON(FILE, true) || {}
- );
+(function(back) {
+ var FILE = "gipy.json";
+ // Load settings
+ var settings = Object.assign({
+ lost_distance: 50,
+ buzz_on_turns: false,
+ disable_bluetooth: true,
+ brightness: 0.5,
+ power_lcd_off: false,
+ },
+ require("Storage").readJSON(FILE, true) || {}
+ );
- function writeSettings() {
- require("Storage").writeJSON(FILE, settings);
- }
+ function writeSettings() {
+ require("Storage").writeJSON(FILE, settings);
+ }
- // Show the menu
- E.showMenu({
- "": { title: "Gipy" },
- "< Back": () => back(),
- "lost distance": {
- value: 50 | settings.lost_distance, // 0| converts undefined to 0
- min: 10,
- max: 500,
- onchange: (v) => {
- settings.max_speed = v;
- writeSettings();
- },
- },
- });
+ // Show the menu
+ E.showMenu({
+ "": {
+ title: "Gipy"
+ },
+ "< Back": () => back(),
+ /*LANG*/"buzz on turns": {
+ value: settings.buzz_on_turns == true,
+ onchange: (v) => {
+ settings.buzz_on_turns = v;
+ writeSettings();
+ }
+ },
+ /*LANG*/"disable bluetooth": {
+ value: settings.disable_bluetooth == true,
+ onchange: (v) => {
+ settings.disable_bluetooth = v;
+ writeSettings();
+ }
+ },
+ "lost distance": {
+ value: settings.lost_distance,
+ min: 10,
+ max: 500,
+ onchange: (v) => {
+ settings.lost_distance = v;
+ writeSettings();
+ },
+ },
+ "brightness": {
+ value: settings.brightness,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ onchange: (v) => {
+ settings.brightness = v;
+ writeSettings();
+ },
+ },
+
+ /*LANG*/"power lcd off": {
+ value: settings.power_lcd_off == true,
+ onchange: (v) => {
+ settings.power_lcd_off = v;
+ writeSettings();
+ }
+ },
+ });
});
diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog
index 0d7c06ab4..921000e82 100644
--- a/apps/gpstrek/ChangeLog
+++ b/apps/gpstrek/ChangeLog
@@ -15,4 +15,7 @@
Save state if route or waypoint has been chosen
0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled
0.10: Adds map view of loaded route
- Automatically search for new waypoint if moving away from current target
\ No newline at end of file
+ Automatically search for new waypoint if moving away from current target
+0.11: Adds configuration
+ Draws direction arrows on route
+ Turn of compass when GPS fix is available
\ No newline at end of file
diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js
index 95db86aaf..eb21498c0 100644
--- a/apps/gpstrek/app.js
+++ b/apps/gpstrek/app.js
@@ -7,25 +7,12 @@ const MODE_SLICES = 2;
const STORAGE = require("Storage");
const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
-const SETTINGS = {
- mapCompass: true,
- mapScale:0.2, //initial value
- mapRefresh:1000, //minimum time in ms between refreshs of the map
- mapChunkSize: 5, //render this many waypoints at a time
- overviewScroll: 30, //scroll this amount on swipe in pixels
- overviewScale: 0.02, //initial value
- refresh:500, //general refresh interval in ms
- refreshLocked:3000, //general refresh interval when Bangle is locked
- cacheMinFreeMem:2000,
- cacheMaxEntries:0,
- minCourseChange: 5, //course change needed in degrees before redrawing the map
- minPosChange: 5, //position change needed in pixels before redrawing the map
- waypointChangeDist: 50, //distance in m to next waypoint before advancing automatically
- queueWaitingTime: 5, // waiting time during processing of task queue items when running with timeouts
- autosearch: true,
- maxDistForAutosearch: 300,
- autosearchLimit: 3
-};
+
+
+const SETTINGS = Object.assign(
+ require('Storage').readJSON("gpstrek.default.json", true) || {},
+ require('Storage').readJSON("gpstrek.json", true) || {}
+);
let init = function(){
global.screen = 1;
@@ -38,7 +25,6 @@ let init = function(){
Bangle.loadWidgets();
WIDGETS.gpstrek.start(false);
- if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 2;
if (!WIDGETS.gpstrek.getState().mode) WIDGETS.gpstrek.getState().mode = MODE_MENU;
};
@@ -184,11 +170,6 @@ let getDoubleLineSlice = function(title1,title2,provider1,provider2){
};
};
-const dot = Graphics.createImage(`
-XX
-XX
-`);
-
const arrow = Graphics.createImage(`
X
XXX
@@ -198,6 +179,14 @@ const arrow = Graphics.createImage(`
XXX XXX
`);
+const thinarrow = Graphics.createImage(`
+ X
+ XXX
+ XX XX
+ XX XX
+XX XX
+`);
+
const cross = Graphics.createImage(`
XX XX
XX XX
@@ -459,7 +448,7 @@ let getMapSlice = function(){
if (!isMapOverview){
drawCurrentPos();
}
- if (!isMapOverview && renderInTimeouts){
+ if (SETTINGS.mapCompass && !isMapOverview && renderInTimeouts){
drawMapCompass();
}
if (renderInTimeouts) drawInterface();
@@ -472,7 +461,8 @@ let getMapSlice = function(){
i:startingIndex,
poly:[],
maxWaypoints: maxWaypoints,
- breakLoop: false
+ breakLoop: false,
+ dist: 0
};
let drawChunk = function(data){
@@ -483,6 +473,7 @@ let getMapSlice = function(){
let last;
let toDraw;
let named = [];
+ let dir = [];
for (let j = 0; j < SETTINGS.mapChunkSize; j++){
data.i = data.i + (reverse?-1:1);
let p = get(route, data.i);
@@ -497,7 +488,17 @@ let getMapSlice = function(){
break;
}
toDraw = Bangle.project(p);
- if (p.name) named.push({i:data.poly.length,n:p.name});
+
+ if (SETTINGS.mapDirection){
+ let lastWp = get(route, data.i - (reverse?-1:1));
+ if (lastWp) data.dist+=distance(lastWp,p);
+ if (!isMapOverview && data.dist > 20/mapScale){
+ dir.push({i:data.poly.length,b:require("graphics_utils").degreesToRadians(bearing(lastWp,p)-(reverse?0:180))});
+ data.dist=0;
+ }
+ }
+ if (p.name)
+ named.push({i:data.poly.length,n:p.name});
data.poly.push(startingPoint.x-toDraw.x);
data.poly.push((startingPoint.y-toDraw.y)*-1);
}
@@ -518,7 +519,11 @@ let getMapSlice = function(){
}
graphics.drawString(c.n, data.poly[c.i] + 10, data.poly[c.i+1]);
}
-
+
+ for (let c of dir){
+ graphics.drawImage(thinarrow, data.poly[c.i], data.poly[c.i+1], {rotate: c.b});
+ }
+
if (finish)
graphics.drawImage(finishIcon, data.poly[data.poly.length - 2] -5, data.poly[data.poly.length - 1] - 4);
else if (last) {
@@ -1254,11 +1259,6 @@ let showMenu = function(){
"Background" : showBackgroundMenu,
"Calibration": showCalibrationMenu,
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});},
- "Info rows" : {
- value : WIDGETS.gpstrek.getState().numberOfSlices,
- min:1,max:6,step:1,
- onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
- },
};
E.showMenu(mainmenu);
@@ -1374,7 +1374,7 @@ const finishData = {
};
let getSliceHeight = function(number){
- return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices);
+ return Math.floor(Bangle.appRect.h/SETTINGS.numberOfSlices);
};
let compassSlice = getCompassSlice();
@@ -1455,7 +1455,6 @@ let updateRouting = function() {
lastSearch = Date.now();
autosearchCounter++;
}
- let counter = 0;
while (hasNext(s.route) && distance(s.currentPos,get(s.route)) < SETTINGS.waypointChangeDist) {
next(s.route);
minimumDistance = Number.MAX_VALUE;
@@ -1479,7 +1478,7 @@ let updateSlices = function(){
slices.push(healthSlice);
slices.push(systemSlice);
slices.push(system2Slice);
- maxSlicePages = Math.ceil(slices.length/s.numberOfSlices);
+ maxSlicePages = Math.ceil(slices.length/SETTINGS.numberOfSlices);
};
let page_slices = 0;
@@ -1515,9 +1514,9 @@ let drawSlices = function(){
if (force){
clear();
}
- let firstSlice = page_slices*s.numberOfSlices;
+ let firstSlice = page_slices*SETTINGS.numberOfSlices;
let sliceHeight = getSliceHeight();
- let slicesToDraw = slices.slice(firstSlice,firstSlice + s.numberOfSlices);
+ let slicesToDraw = slices.slice(firstSlice,firstSlice + SETTINGS.numberOfSlices);
for (let slice of slicesToDraw) {
g.reset();
if (!slice.refresh || slice.refresh() || force)
diff --git a/apps/gpstrek/default.json b/apps/gpstrek/default.json
new file mode 100644
index 000000000..aa8d5ecb1
--- /dev/null
+++ b/apps/gpstrek/default.json
@@ -0,0 +1,21 @@
+{
+ "mapCompass": true,
+ "mapScale":0.5,
+ "mapRefresh":1000,
+ "mapChunkSize": 15,
+ "mapDirection": true,
+ "overviewScroll": 30,
+ "overviewScale": 0.02,
+ "refresh":500,
+ "refreshLocked":3000,
+ "cacheMinFreeMem":2000,
+ "cacheMaxEntries":0,
+ "minCourseChange": 5,
+ "minPosChange": 5,
+ "waypointChangeDist": 50,
+ "queueWaitingTime": 5,
+ "autosearch": true,
+ "maxDistForAutosearch": 300,
+ "autosearchLimit": 3,
+ "numberOfSlices": 3
+}
diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json
index 0ec3a8bfe..ec953e6e7 100644
--- a/apps/gpstrek/metadata.json
+++ b/apps/gpstrek/metadata.json
@@ -1,7 +1,7 @@
{
"id": "gpstrek",
"name": "GPS Trekking",
- "version": "0.10",
+ "version": "0.11",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png",
"screenshots": [{"url":"screenInit.png"},{"url":"screenMenu.png"},{"url":"screenMap.png"},{"url":"screenLost.png"},{"url":"screenOverview.png"},{"url":"screenOverviewScroll.png"},{"url":"screenSlices.png"},{"url":"screenSlices2.png"},{"url":"screenSlices3.png"}],
@@ -12,8 +12,13 @@
"interface" : "interface.html",
"storage": [
{"name":"gpstrek.app.js","url":"app.js"},
+ {"name":"gpstrek.settings.js","url":"settings.js"},
+ {"name":"gpstrek.default.json","url":"default.json"},
{"name":"gpstrek.wid.js","url":"widget.js"},
{"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
],
- "data": [{"name":"gpstrek.state.json"}]
+ "data": [
+ {"name":"gpstrek.state.json"},
+ {"name":"gpstrek.json"}
+ ]
}
diff --git a/apps/gpstrek/settings.js b/apps/gpstrek/settings.js
new file mode 100644
index 000000000..1510bcba4
--- /dev/null
+++ b/apps/gpstrek/settings.js
@@ -0,0 +1,162 @@
+(function(back) {
+ const FILE="gpstrek.json";
+ let settings;
+
+ function writeSettings(key, value) {
+ var s = require('Storage').readJSON(FILE, true) || {};
+ s[key] = value;
+ require('Storage').writeJSON(FILE, s);
+ readSettings();
+ }
+
+ function readSettings(){
+ settings = Object.assign(
+ require('Storage').readJSON("gpstrek.default.json", true) || {},
+ require('Storage').readJSON(FILE, true) || {}
+ );
+ }
+
+
+ function showMapMenu(){
+ var menu = {
+ '': { 'title': 'Map', back: showMainMenu },
+ 'Show compass on map': {
+ value: !!settings.mapCompass,
+ onchange: v => {
+ writeSettings("mapCompass",v);
+ },
+ },
+ 'Initial map scale': {
+ value: settings.mapScale,
+ min: 0.01,max: 2, step:0.01,
+ onchange: v => {
+ writeSettings("mapScale",v);
+ },
+ },
+ 'Rendered waypoints': {
+ value: settings.mapChunkSize,
+ min: 5,max: 60, step:5,
+ onchange: v => {
+ writeSettings("mapChunkSize",v);
+ }
+ },
+ 'Overview scroll': {
+ value: settings.overviewScroll,
+ min: 10,max: 100, step:10,
+ format: v => v + "px",
+ onchange: v => {
+ writeSettings("overviewScroll",v);
+ }
+ },
+ 'Initial overview scale': {
+ value: settings.overviewScale,
+ min: 0.005,max: 0.1, step:0.005,
+ onchange: v => {
+ writeSettings("overviewScale",v);
+ }
+ },
+ 'Show direction': {
+ value: !!settings.mapDirection,
+ onchange: v => {
+ writeSettings("mapDirection",v);
+ }
+ }
+ };
+ E.showMenu(menu);
+ }
+
+ function showRoutingMenu(){
+ var menu = {
+ '': { 'title': 'Routing', back: showMainMenu },
+ 'Auto search closest waypoint': {
+ value: !!settings.autosearch,
+ onchange: v => {
+ writeSettings("autosearch",v);
+ },
+ },
+ 'Auto search limit': {
+ value: settings.autosearchLimit,
+ onchange: v => {
+ writeSettings("autosearchLimit",v);
+ },
+ },
+ 'Waypoint change distance': {
+ value: settings.waypointChangeDist,
+ format: v => v + "m",
+ min: 5,max: 200, step:5,
+ onchange: v => {
+ writeSettings("waypointChangeDist",v);
+ },
+ }
+ };
+ E.showMenu(menu);
+ }
+
+ function showRefreshMenu(){
+ var menu = {
+ '': { 'title': 'Refresh', back: showMainMenu },
+ 'Unlocked refresh': {
+ value: settings.refresh,
+ format: v => v + "ms",
+ min: 250,max: 5000, step:250,
+ onchange: v => {
+ writeSettings("refresh",v);
+ }
+ },
+ 'Locked refresh': {
+ value: settings.refreshLocked,
+ min: 1000,max: 60000, step:1000,
+ format: v => v + "ms",
+ onchange: v => {
+ writeSettings("refreshLocked",v);
+ }
+ },
+ 'Minimum refresh': {
+ value: settings.mapRefresh,
+ format: v => v + "ms",
+ min: 250,max: 5000, step:250,
+ onchange: v => {
+ writeSettings("mapRefresh",v);
+ }
+ },
+ 'Minimum course change': {
+ value: settings.minCourseChange,
+ min: 0,max: 180, step:1,
+ format: v => v + "°",
+ onchange: v => {
+ writeSettings("minCourseChange",v);
+ }
+ },
+ 'Minimum position change': {
+ value: settings.minPosChange,
+ min: 0,max: 50, step:1,
+ format: v => v + "px",
+ onchange: v => {
+ writeSettings("minPosChange",v);
+ }
+ }
+ };
+ E.showMenu(menu);
+ }
+
+
+ function showMainMenu(){
+ var mainmenu = {
+ '': { 'title': 'GPS Trekking', back: back },
+ 'Map': showMapMenu,
+ 'Routing': showRoutingMenu,
+ 'Refresh': showRefreshMenu,
+ "Info rows" : {
+ value : settings.numberOfSlices,
+ min:1,max:6,step:1,
+ onchange : v => {
+ writeSettings("numberOfSlices",v);
+ }
+ },
+ };
+ E.showMenu(mainmenu);
+ }
+
+ readSettings();
+ showMainMenu();
+})
\ No newline at end of file
diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js
index 6887486bc..44acdc722 100644
--- a/apps/gpstrek/widget.js
+++ b/apps/gpstrek/widget.js
@@ -45,7 +45,15 @@ function onPulse(e){
}
function onGPS(fix) {
- if(fix.fix) state.currentPos = fix;
+ if(fix.fix) {
+ state.currentPos = fix;
+ if (Bangle.isCompassOn()){
+ Bangle.setCompassPower(0, "gpstrek");
+ state.compassSamples = new Array(SAMPLES).fill(0)
+ }
+ } else {
+ Bangle.setCompassPower(1, "gpstrek");
+ }
}
let radians = function(a) {
diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog
index 12740959a..da68bc0e7 100644
--- a/apps/health/ChangeLog
+++ b/apps/health/ChangeLog
@@ -22,3 +22,7 @@
0.21: Update boot.min.js.
0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435)
0.23: Fix HRM logic
+0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written)
+0.25: lib.read* methods now return correctly scaled movement
+ movement graph in app is now an average, not sum
+ fix 11pm slot for daily HRM
\ No newline at end of file
diff --git a/apps/health/app.js b/apps/health/app.js
index bd708207b..fdc69dd28 100644
--- a/apps/health/app.js
+++ b/apps/health/app.js
@@ -59,7 +59,7 @@ function hrmPerHour() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "hrmPerHour";
var data = new Uint16Array(24);
- var cnt = new Uint8Array(23);
+ var cnt = new Uint8Array(24);
require("health").readDay(new Date(), h=>{
data[h.hr]+=h.bpm;
if (h.bpm) cnt[h.hr]++;
@@ -87,7 +87,12 @@ function movementPerHour() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "movementPerHour";
var data = new Uint16Array(24);
- require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
+ var cnt = new Uint8Array(24);
+ require("health").readDay(new Date(), h=>{
+ data[h.hr]+=h.movement
+ cnt[h.hr]++;
+ });
+ data.forEach((d,i)=>data[i] = d/cnt[i]);
setButton(menuMovement);
barChart(/*LANG*/"HOUR", data);
}
@@ -96,7 +101,12 @@ function movementPerDay() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "movementPerDay";
var data = new Uint16Array(31);
- require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
+ var cnt = new Uint8Array(31);
+ require("health").readDailySummaries(new Date(), h=>{
+ data[h.hr]+=h.movement
+ cnt[h.hr]++;
+ });
+ data.forEach((d,i)=>data[i] = d/cnt[i]);
setButton(menuMovement);
barChart(/*LANG*/"DAY", data);
}
diff --git a/apps/health/boot.js b/apps/health/boot.js
index 62e8b87ab..66b4acda6 100644
--- a/apps/health/boot.js
+++ b/apps/health/boot.js
@@ -52,7 +52,7 @@ Bangle.on("health", health => {
return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps
health.bpm, // 8 bit bpm
- Math.min(health.movement / 8, 255)); // movement
+ Math.min(health.movement, 255)); // movement
}
var rec = getRecordIdx(d);
@@ -68,6 +68,12 @@ Bangle.on("health", health => {
require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header
}
var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN);
+
+ // scale down reported movement value in order to fit it within a
+ // uint8 DB field
+ health = Object.assign({}, health);
+ health.movement /= 8;
+
require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN);
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
// we're at the end of the day. Read in all of the data for the day and sum it up
@@ -82,10 +88,10 @@ Bangle.on("health", health => {
var dt = f.substr(recordPos, DB_RECORD_LEN);
if (dt!="\xFF\xFF\xFF\xFF") {
health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1);
- health.movement += dt.charCodeAt(2);
- health.movCnt++;
var bpm = dt.charCodeAt(2);
health.bpm += bpm;
+ health.movement += dt.charCodeAt(3);
+ health.movCnt++;
if (bpm) health.bpmCnt++;
}
recordPos -= DB_RECORD_LEN;
diff --git a/apps/health/boot.min.js b/apps/health/boot.min.js
index 651231195..0d1a80f4c 100644
--- a/apps/health/boot.min.js
+++ b/apps/health/boot.min.js
@@ -1,5 +1,5 @@
-function l(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateMath.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,
-"health")});90>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0k;k++)e=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=e&&(a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1),a.movement+=e.charCodeAt(2),
-a.movCnt++,e=e.charCodeAt(2),a.bpm+=e,e&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
+function m(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateBangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",d);Bangle.on("HRM",b=>{90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement,255))}var b=new Date(Date.now()-59E4);a&&0k;k++){e=g.substr(h,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var l=e.charCodeAt(2);a.bpm+=l;a.movement+=e.charCodeAt(3);a.movCnt++;
+l&&a.bpmCnt++}h-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
\ No newline at end of file
diff --git a/apps/health/lib.js b/apps/health/lib.js
index 3a52ad59f..7ecbd5bff 100644
--- a/apps/health/lib.js
+++ b/apps/health/lib.js
@@ -29,7 +29,7 @@ exports.readAllRecords = function(d, cb) {
day:day+1, hr : hr, min:m*10,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
- movement : h.charCodeAt(3)
+ movement : h.charCodeAt(3)*8
});
}
idx += DB_RECORD_LEN;
@@ -53,7 +53,7 @@ exports.readDailySummaries = function(d, cb) {
day:day+1,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
- movement : h.charCodeAt(3)
+ movement : h.charCodeAt(3)*8
});
}
idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN;
@@ -75,7 +75,7 @@ exports.readDay = function(d, cb) {
hr : hr, min:m*10,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
- movement : h.charCodeAt(3)
+ movement : h.charCodeAt(3)*8
});
}
idx += DB_RECORD_LEN;
diff --git a/apps/health/lib.min.js b/apps/health/lib.min.js
index 4bdc4c0fb..5d0bed2c2 100644
--- a/apps/health/lib.min.js
+++ b/apps/health/lib.min.js
@@ -1,3 +1,3 @@
-function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:g.charCodeAt(3)});c+=
-4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\u00ff\u00ff\u00ff\u00ff"!=e&&f({hr:d,
-min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:e.charCodeAt(3)});a+=4}}}
\ No newline at end of file
+function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\xff\xff\xff\xff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});c+=
+4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\xff\xff\xff\xff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:8*b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\xff\xff\xff\xff"!=e&&
+f({hr:d,min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:8*e.charCodeAt(3)});a+=4}}}
\ No newline at end of file
diff --git a/apps/health/metadata.json b/apps/health/metadata.json
index 30e4b4276..5ff3bb3a0 100644
--- a/apps/health/metadata.json
+++ b/apps/health/metadata.json
@@ -2,7 +2,7 @@
"id": "health",
"name": "Health Tracking",
"shortName": "Health",
- "version": "0.23",
+ "version": "0.25",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",
diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog
index 8bad496bf..504f747bd 100644
--- a/apps/iconlaunch/ChangeLog
+++ b/apps/iconlaunch/ChangeLog
@@ -21,4 +21,6 @@
0.15: Ensure that we hide widgets if in fullscreen mode
(So that widgets are still hidden if launcher is fast-loaded)
0.16: Use firmware provided E.showScroller method
-0.17: fix fullscreen with oneClickExit
+0.17: fix fullscreen with oneClickExit
+0.18: Better performance
+0.19: Remove 'jit' keyword as 'for(..of..)' is not supported (fix #2937)
\ No newline at end of file
diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js
index 9f8cedb0f..f7d5b7bf1 100644
--- a/apps/iconlaunch/app.js
+++ b/apps/iconlaunch/app.js
@@ -9,7 +9,6 @@
timeOut:"Off"
}, s.readJSON("iconlaunch.json", true) || {});
-
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -19,9 +18,9 @@
let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
let launchHash = s.hash(/\.info/);
if (launchCache.hash!=launchHash) {
- launchCache = {
- hash : launchHash,
- apps : s.list(/\.info$/)
+ launchCache = {
+ hash : launchHash,
+ apps : s.list(/\.info$/)
.map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};})
.filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type))
.sort((a,b)=>{
@@ -34,42 +33,65 @@
s.writeJSON("iconlaunch.cache.json", launchCache);
}
+ // cache items
+ const ICON_MISSING = s.read("iconlaunch.na.img");
+ let count = 0;
+
let selectedItem = -1;
const R = Bangle.appRect;
const iconSize = 48;
const appsN = Math.floor(R.w / iconSize);
- const whitespace = (R.w - appsN * iconSize) / (appsN + 1);
+ const whitespace = Math.floor((R.w - appsN * iconSize) / (appsN + 1));
+ const iconYoffset = Math.floor(whitespace/4)-1;
const itemSize = iconSize + whitespace;
+ launchCache.items = {};
+ for (let c of launchCache.apps){
+ let i = Math.floor(count/appsN);
+ if (!launchCache.items[i])
+ launchCache.items[i] = {};
+ launchCache.items[i][(count%3)] = c;
+ count++;
+ }
+
+ let texted;
let drawItem = function(itemI, r) {
- g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
- let x = 0;
- for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) {
- if (!launchCache.apps[i]) break;
- x += whitespace;
- if (!launchCache.apps[i].icon) {
- g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2);
- } else {
- if (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon);
- g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y);
- }
- if (selectedItem == i) {
- g.drawRect(
- x + r.x - 1,
- r.y - 1,
- x + r.x + iconSize + 1,
- r.y + iconSize + 1
- );
- }
- x += iconSize;
+ let x = whitespace;
+ let i = itemI * appsN - 1;
+ let selectedApp;
+ let c;
+ let selectedRect;
+ let item = launchCache.items[itemI];
+ if (texted == itemI){
+ g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
+ texted = undefined;
+ }
+ for (c of item) {
+ i++;
+ let id = c.icondata || (c.iconData = (c.icon ? s.read(c.icon) : ICON_MISSING));
+ g.drawImage(id,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize);
+ if (selectedItem == i) {
+ selectedApp = c;
+ selectedRect = [
+ x + r.x - 1,
+ r.y + iconYoffset - 1,
+ x + r.x + iconSize,
+ r.y + iconYoffset + iconSize
+ ];
+ }
+ x += iconSize + whitespace;
+ }
+ if (selectedRect) {
+ g.drawRect.apply(null, selectedRect);
+ drawText(itemI, r.y, selectedApp);
+ texted=itemI;
}
- drawText(itemI, r.y);
};
- let drawText = function(i, appY) {
- const selectedApp = launchCache.apps[selectedItem];
+ let drawText = function(i, appY, selectedApp) {
+ "jit";
const idy = (selectedItem - (selectedItem % 3)) / 3;
- if (!selectedApp || i != idy) return;
+ if (i != idy) return;
appY = appY + itemSize/2;
g.setFontAlign(0, 0, 0);
g.setFont("12x20");
@@ -122,21 +144,21 @@
},
btn:Bangle.showClock
};
-
+
//work both the fullscreen and the oneClickExit
if( settings.fullscreen && settings.oneClickExit)
{
- idWatch=setWatch(function(e) {
+ idWatch=setWatch(function(e) {
Bangle.showClock();
}, BTN, {repeat:false, edge:'rising' });
-
+
}
- else if( settings.oneClickExit )
+ else if( settings.oneClickExit )
{
options.back=Bangle.showClock;
}
-
+
let scroller = E.showScroller(options);
@@ -151,7 +173,7 @@
};
let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } };
-
+
Bangle.on("swipe", swipeHandler)
Bangle.on("drag", updateTimeout);
Bangle.on("touch", updateTimeout);
diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json
index 35a7907bd..acf46a431 100644
--- a/apps/iconlaunch/metadata.json
+++ b/apps/iconlaunch/metadata.json
@@ -2,7 +2,7 @@
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
- "version": "0.17",
+ "version": "0.19",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",
@@ -10,7 +10,8 @@
"supports": ["BANGLEJS2"],
"storage": [
{ "name": "iconlaunch.app.js", "url": "app.js" },
- { "name": "iconlaunch.settings.js", "url": "settings.js" }
+ { "name": "iconlaunch.settings.js", "url": "settings.js" },
+ { "name": "iconlaunch.na.img", "url": "na.img" }
],
"data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
diff --git a/apps/iconlaunch/na.img b/apps/iconlaunch/na.img
new file mode 100644
index 000000000..10f4a8f82
Binary files /dev/null and b/apps/iconlaunch/na.img differ
diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js
index f4c0599f7..3278075e4 100644
--- a/apps/iconlaunch/settings.js
+++ b/apps/iconlaunch/settings.js
@@ -16,8 +16,7 @@
}
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
const appMenu = {
- "": { "title": /*LANG*/"Launcher" },
- /*LANG*/"< Back": back,
+ "": { "title": /*LANG*/"Launcher", back: back },
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
onchange: (m) => {
diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog
index defae902b..28344678a 100644
--- a/apps/kbmulti/ChangeLog
+++ b/apps/kbmulti/ChangeLog
@@ -4,3 +4,6 @@
0.04: Allow moving the cursor
0.05: Switch swipe directions for Caps Lock and moving cursor.
0.06: Add ability to auto-lowercase after a capital letter insertion.
+0.07: Add compatability with `backswipe` app by using `Bangle.prependListener()` and `E.stopEventPropagation`- requires fw 2v19 or cutting
+ edge versions of 2v18. Falls back on `Bangle.on()` for backwards
+ compatability.
diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js
index 505132040..f979e4473 100644
--- a/apps/kbmulti/lib.js
+++ b/apps/kbmulti/lib.js
@@ -154,6 +154,7 @@ exports.input = function(options) {
displayText(false);
}
}
+ E.stopEventPropagation&&E.stopEventPropagation();
}
function onHelp(resolve,reject) {
@@ -161,7 +162,7 @@ exports.input = function(options) {
E.showPrompt(
helpMessage, {title: "Help", buttons : {"Ok":true}}
).then(function(v) {
- Bangle.on('swipe', onSwipe);
+ if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);}
generateLayout(resolve,reject);
layout.render();
});
@@ -208,7 +209,7 @@ exports.input = function(options) {
} else {
generateLayout(resolve,reject);
displayText(false);
- Bangle.on('swipe', onSwipe);
+ if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);}
layout.render();
}
});
diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json
index 0b44b0306..210646a01 100644
--- a/apps/kbmulti/metadata.json
+++ b/apps/kbmulti/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbmulti",
"name": "Multitap keyboard",
- "version":"0.06",
+ "version":"0.07",
"description": "A library for text input via multitap/T9 style keypad",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kbswipe/ChangeLog b/apps/kbswipe/ChangeLog
index 38d71986e..51401deda 100644
--- a/apps/kbswipe/ChangeLog
+++ b/apps/kbswipe/ChangeLog
@@ -6,3 +6,5 @@
0.06: Support input of numbers and uppercase characters.
0.07: Support input of symbols.
0.08: Redone patterns a,e,m,w,z.
+0.09: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js
index 7d05d7a8e..bed171d72 100644
--- a/apps/kbswipe/lib.js
+++ b/apps/kbswipe/lib.js
@@ -253,28 +253,38 @@ exports.input = function(options) {
};
Bangle.drawWidgets();
+ let dragHandlerKB = e=>{
+ "ram";
+ if (isInside(R, e)) {
+ if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
+ lastDrag = e.b ? e : 0;
+ }
+ }
+
+ let touchHandlerKB = (n,e) => {
+ if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
+ // touch inside widget
+ cycleInput();
+ } else if (isInside(R, e)) {
+ // touch inside app area
+ show();
+ }
+ }
+
+ let catchSwipe = ()=>{
+ E.stopEventPropagation&&E.stopEventPropagation();
+ };
+
return new Promise((resolve,reject) => {
- Bangle.setUI({mode:"custom", drag:e=>{
- "ram";
- if (isInside(R, e)) {
- if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
- lastDrag = e.b ? e : 0;
- }
- },touch:(n,e) => {
- if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
- // touch inside widget
- cycleInput();
- } else if (isInside(R, e)) {
- // touch inside app area
- show();
- }
- }, back:()=>{
+ Bangle.setUI({mode:"custom", drag:dragHandlerKB, touch:touchHandlerKB, back:()=>{
delete WIDGETS.kbswipe;
Bangle.removeListener("stroke", strokeHandler);
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
if (flashInterval) clearInterval(flashInterval);
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
}});
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
});
};
diff --git a/apps/kbswipe/metadata.json b/apps/kbswipe/metadata.json
index 3f3fbffa3..22e1e1431 100644
--- a/apps/kbswipe/metadata.json
+++ b/apps/kbswipe/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbswipe",
"name": "Swipe keyboard",
- "version":"0.08",
+ "version":"0.09",
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kbtouch/ChangeLog b/apps/kbtouch/ChangeLog
index 5bd2159e6..cb8e5cda6 100644
--- a/apps/kbtouch/ChangeLog
+++ b/apps/kbtouch/ChangeLog
@@ -1,3 +1,5 @@
0.01: New App!
0.02: Introduced settings to customize the layout and functionality of the keyboard.
0.03: Convert Yes/No On/Off in settings to checkboxes
+0.04: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/kbtouch/lib.js b/apps/kbtouch/lib.js
index db90440b9..4f064cfc7 100644
--- a/apps/kbtouch/lib.js
+++ b/apps/kbtouch/lib.js
@@ -161,8 +161,11 @@ function draw() {
},back:()=>{
clearInterval(flashInterval);
Bangle.setUI();
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(text);
}});
+ let catchSwipe = ()=>{E.stopEventPropagation&&E.stopEventPropagation();};
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
});
};
diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json
index 31dc8c9a8..349726498 100644
--- a/apps/kbtouch/metadata.json
+++ b/apps/kbtouch/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbtouch",
"name": "Touch keyboard",
- "version":"0.03",
+ "version":"0.04",
"description": "A library for text input via onscreen keyboard",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kineticscroll/ChangeLog b/apps/kineticscroll/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/kineticscroll/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/kineticscroll/README.md b/apps/kineticscroll/README.md
new file mode 100644
index 000000000..be451a7f4
--- /dev/null
+++ b/apps/kineticscroll/README.md
@@ -0,0 +1,7 @@
+# Kinetic scrolling
+
+This patches the default scroller implementation to use kinetic scrolling. It is based on the original implementation.
+
+## Creator
+
+[halemmerich](https://github.com/halemmerich)
diff --git a/apps/kineticscroll/app.png b/apps/kineticscroll/app.png
new file mode 100644
index 000000000..f3184b5c0
Binary files /dev/null and b/apps/kineticscroll/app.png differ
diff --git a/apps/kineticscroll/boot.js b/apps/kineticscroll/boot.js
new file mode 100644
index 000000000..1f1b7923a
--- /dev/null
+++ b/apps/kineticscroll/boot.js
@@ -0,0 +1,181 @@
+(function() {
+ E.showScroller = function(options) {
+ /* options = {
+ h = height
+ c = # of items
+ scroll = initial scroll position
+ scrollMin = minimum scroll amount (can be negative)
+ draw = function(idx, rect)
+ remove = function()
+ select = function(idx, touch)
+ }
+
+ returns {
+ scroll: int // current scroll amount
+ draw: function() // draw all
+ drawItem : function(idx) // draw specific item
+ isActive : function() // is this scroller still active?
+ }
+
+ */
+ if (!options) return Bangle.setUI(); // remove existing handlers
+
+ const MAX_VELOCITY=100;
+ let scheduledDraw;
+ let velocity = 0;
+ let accDy = 0;
+ let scheduledBrake = setInterval(()=>{velocity*=0.9;}, 50);
+ let lastDragStart = 0;
+ let R = Bangle.appRect;
+ let menuScrollMin = 0|options.scrollMin;
+ let menuScrollMax = options.h*options.c - R.h;
+ if (menuScrollMax{
+ if (e.y=0) && i {
+ g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2);
+ var a = YtoIdx(R.y);
+ var b = Math.min(YtoIdx(R.y2),options.c-1);
+ for (var i=a;i<=b;i++)
+ options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ }
+
+ const draw = () => {
+ let dy = velocity;
+ if (s.scroll - dy > menuScrollMax){
+ dy = s.scroll - menuScrollMax;
+ velocity = 0;
+ }
+ if (s.scroll - dy < menuScrollMin){
+ dy = s.scroll - menuScrollMin;
+ velocity = 0;
+ }
+
+ s.scroll -= dy;
+
+ let oldScroll = rScroll;
+ rScroll = s.scroll &~1;
+ let d = oldScroll-rScroll;
+
+ if (Math.abs(velocity) > 0.01)
+ scheduledDraw = setTimeout(draw,0);
+ else
+ scheduledDraw = undefined;
+
+ if (!d) {
+ return;
+ }
+ g.reset().setClipRect(R.x,R.y,R.x2,R.y2).scroll(0,d);
+ if (d < 0) {
+ let y = Math.max(R.y2-(1-d), R.y);
+ g.setClipRect(R.x,y,R.x2,R.y2);
+ let i = YtoIdx(y);
+
+ for (y = idxToY(i);y < R.y2;y+=options.h) {
+ options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+ i++;
+ }
+ } else { // d>0
+ let y = Math.min(R.y+d, R.y2);
+ g.setClipRect(R.x,R.y,R.x2,y);
+ let i = YtoIdx(y);
+ y = idxToY(i);
+
+ for (y = idxToY(i);y > R.y-options.h;y-=options.h) {
+ options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+ i--;
+ }
+ }
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ };
+
+ const dragHandler = e=>{
+ if ((velocity <0 && e.dy>0) || (velocity > 0 && e.dy<0)){
+ velocity *= -1;
+ accDy = 5 * velocity;
+ }
+ //velocity += e.dy * (Date.now() - lastDrag);
+ if (e.b > 0){
+ if (!lastDragStart){
+ lastDragStart = Date.now();
+ velocity = 0;
+ accDy = 0;
+ }
+ accDy += e.dy;
+ }
+ velocity = accDy / (Date.now() - lastDragStart) * MAX_VELOCITY;
+
+ if (lastDragStart && e.b == 0){
+ accDy = 0;
+ lastDragStart = 0;
+ }
+
+ velocity = E.clip(velocity,-MAX_VELOCITY,MAX_VELOCITY);
+ lastDrag=Date.now();
+ if (!scheduledDraw){
+ scheduledDraw = setTimeout(draw,0);
+ }
+ };
+
+ let uiOpts = {
+ mode : "custom",
+ back : options.back,
+ drag : dragHandler,
+ touch : touchHandler,
+ redraw : uiDraw
+ }
+
+ if (options.remove) uiOpts.remove = () => {
+ if (scheduledDraw)
+ clearTimeout(scheduledDraw);
+ clearInterval(scheduledBrake);
+ options.remove();
+ }
+
+ Bangle.setUI(uiOpts);
+
+
+
+ function idxToY(i) {
+ return i*options.h + R.y - rScroll;
+ }
+ function YtoIdx(y) {
+ return Math.floor((y + rScroll - R.y)/options.h);
+ }
+
+ let s = {
+ scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
+ draw : () => {
+ g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2);
+ let a = YtoIdx(R.y);
+ let b = Math.min(YtoIdx(R.y2),options.c-1);
+ for (let i=a;i<=b;i++)
+ options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ }, drawItem : i => {
+ let y = idxToY(i);
+ g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2));
+ options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ }, isActive : () => Bangle.uiRedraw == uiDraw
+ };
+
+ let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
+ s.draw(); // draw the full scroller
+ g.flip(); // force an update now to make this snappier
+ return s;
+ };
+})();
diff --git a/apps/kineticscroll/boot.min.js b/apps/kineticscroll/boot.min.js
new file mode 100644
index 000000000..42f0afa67
--- /dev/null
+++ b/apps/kineticscroll/boot.min.js
@@ -0,0 +1,4 @@
+(function(){E.showScroller=function(c){function k(a){return a*c.h+b.y-l}function h(a){return Math.floor((a+l-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,m=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,n=0|c.scrollMin,r=c.h*c.c-b.h;r{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);for(var a=h(b.y),d=Math.min(h(b.y2),c.c-1);a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},u=()=>{var a=e;f.scroll-a>r&&
+(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=h(a);for(a=k(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-
+1)}};let v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0a.dy)e*=-1,m=5*e;0{if(!(d.yn||0<=a)&&a{p&&clearTimeout(p);clearInterval(w);c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,
+n,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=h(b.y);let d=Math.min(h(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=k(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.uiRedraw==t},l=f.scroll&-2;f.draw();g.flip();return f}})()
\ No newline at end of file
diff --git a/apps/kineticscroll/metadata.json b/apps/kineticscroll/metadata.json
new file mode 100644
index 000000000..022d38291
--- /dev/null
+++ b/apps/kineticscroll/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "kineticscroll",
+ "name": "Kinetic Scroll",
+ "shortName":"Kinetic Scroll",
+ "version":"0.01",
+ "description": "Replacement for the system scroller with kinetic scrolling.",
+ "icon": "app.png",
+ "type": "bootloader",
+ "tags": "system",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"kineticscroll.boot.js","url":"boot.min.js"}
+ ]
+}
diff --git a/apps/lunaclock/ChangeLog b/apps/lunaclock/ChangeLog
new file mode 100644
index 000000000..2286a7f70
--- /dev/null
+++ b/apps/lunaclock/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
\ No newline at end of file
diff --git a/apps/lunaclock/README.md b/apps/lunaclock/README.md
new file mode 100644
index 000000000..0b7555b29
--- /dev/null
+++ b/apps/lunaclock/README.md
@@ -0,0 +1,9 @@
+# Luna Clock
+
+
+
+Simple clock face inspired by the moon.
+
+## Creator
+
+NovaDawn999
\ No newline at end of file
diff --git a/apps/lunaclock/app-icon.js b/apps/lunaclock/app-icon.js
new file mode 100644
index 000000000..d17110a5a
--- /dev/null
+++ b/apps/lunaclock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEIf4A/AH4AJiIABiAVRiUz/4AB+cyDJ8TCoX/mc/AQMgCxkjCwgAB+YCBDBcDCwYXCn4CC+ZKJgQWEFYQWCA4MyC5EvC5HzA4U/JI4uEIAQWBD4k/+QuKLYZDCDwQFCGAsBIgoBBFIIGBPIf/GAqMFDIQvDHARjBSQp1EFAIpCL4YGEmBGEE4akEIYYIDPIhGBiYwDmTSEgUiAwMiAAMzJAUf+ZgD+cCCwX/iMjCQMxiczmUDSIUvmIXDmIXBmchmcTn8jmcgJAUgC4U/iLXC+ciC4UgCgMyOwMRC4UwiJ2BLAIXDAgJHBCgReCEAQXBiUACgURWoQEBdAYvBCoIXL+TDBiAWCegUgicBC4cTC4IPBkE/OoM/AgK6EBIMQC4ixBH4ISCmUSkEvBoJ3BJAR9CBIMvC4KHCl4YCiESiYXBSAUBWwIRBNgIXEkZbCmLIBRYchgUykMjAILwBgLeBkAhCLAIEBUwK3BiEikUykRJCiEAiQDBAQJYDAgRABiQVBCwcyd4MCiIHCAAMhT4YACCwIXCkUhC4MBiQgDCAQFCkYVCAAhGBC4MBEIQZEFggWHAARrCkQVELYMhMAICBiJFCC4gMBJYI0CCoInCiAlBCooAEDIIUBHwsBFwIXKDAITBQAIECBAIWMJYSBDGAYXNIAaFFACAtELZpUBHoIBBDAIZBBYJ4CEA0TawMyaQQXBmUwAQMgiYKBmIXFgTjEkMgA4QXBcgQbBNCoAEA=="))
\ No newline at end of file
diff --git a/apps/lunaclock/app.js b/apps/lunaclock/app.js
new file mode 100644
index 000000000..e73467bc1
--- /dev/null
+++ b/apps/lunaclock/app.js
@@ -0,0 +1,72 @@
+var MYIMG = {
+ width : 176, height : 176, bpp : 4,
+ buffer : require("heatshrink").decompress(atob("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"))
+};
+
+//var IMAGEWIDTH = 176;
+//var IMAGEHEIGHT = 109;
+var IMAGEWIDTH = 176;
+var IMAGEHEIGHT = 176;
+
+Graphics.prototype.setFontcustom = function() {
+ // Actual height 58 (57 - 0)
+ return this.setFontCustom(
+ E.toString(require('heatshrink').decompress(atob('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'))),
+ 32,
+ atob("EA4RPC4uLQoZGhwaDxkOKCodHx4pLCMdHCMODx4mHyE0LSgrKiAfLCsZJDImPy4rJC8yHiYoLkEyLiQUFBM="),
+ 58|65536
+ );
+};
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+function draw() {
+ //clock pos
+ var x = g.getWidth()/2;
+ var y = 105;
+ g.setColor("#000000"); //color of clock text
+ //draw clock
+ g.drawImage(MYIMG,0,g.getHeight()-IMAGEHEIGHT, 1);
+ //get time/date
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var dateStr = require("locale").date(date).toUpperCase();
+ var dowStr = require("locale").dow(date).toUpperCase();
+ // draw time
+ g.setFontAlign(0,0).setFont("custom");
+ g.drawString(timeStr,x,y);
+ // draw date
+ g.setColor(1,1,1);
+ var dateFormatted = dowStr + ", " + dateStr;
+ g.setFont("6x8");
+ g.drawString(dateFormatted,g.getWidth()/2,g.getHeight() - 8);
+ Bangle.drawWidgets();
+ // queue draw in one minute
+ queueDraw();
+}
+
+
+g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
+draw();
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw();
+ } else {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/lunaclock/app.png b/apps/lunaclock/app.png
new file mode 100644
index 000000000..314cf9cca
Binary files /dev/null and b/apps/lunaclock/app.png differ
diff --git a/apps/lunaclock/metadata.json b/apps/lunaclock/metadata.json
new file mode 100644
index 000000000..4e797d8e7
--- /dev/null
+++ b/apps/lunaclock/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "lunaclock",
+ "name": "Luna Clock",
+ "version": "0.01",
+ "description": "Simple clock face inspired by the moon.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"lunaclock.app.js","url":"app.js"},
+ {"name":"lunaclock.img","url":"app-icon.js","evaluate":true}
+ ]
+}
\ No newline at end of file
diff --git a/apps/lunaclock/screenshot.png b/apps/lunaclock/screenshot.png
new file mode 100644
index 000000000..62fd91ecf
Binary files /dev/null and b/apps/lunaclock/screenshot.png differ
diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog
index 832c1d45c..47aa51107 100644
--- a/apps/messagesoverlay/ChangeLog
+++ b/apps/messagesoverlay/ChangeLog
@@ -2,3 +2,6 @@
0.02: Fix touch/drag/swipe handlers not being restored correctly if a message is removed
0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible.
0.04: Use the event mechanism for getting messages
+0.05: Fix the overlay keeping the LCD on
+0.06: Better low memory handling
+ Fix first message beeing displayed again on unlock
diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js
index 0ebe78918..6767cfbce 100644
--- a/apps/messagesoverlay/lib.js
+++ b/apps/messagesoverlay/lib.js
@@ -1,3 +1,5 @@
+const MIN_FREE_MEM = 1000;
+const LOW_MEM = 2000;
const ovrx = 10;
const ovry = 10;
const ovrw = g.getWidth()-2*ovrx;
@@ -28,6 +30,7 @@ let callInProgress = false;
let show = function(ovr){
let img = ovr;
+ LOG("show", img.getBPP());
if (ovr.getBPP() == 1) {
img = ovr.asImage();
img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]);
@@ -162,7 +165,9 @@ let showMessage = function(ovr, msg) {
drawMessage(ovr, msg);
};
-let drawBorder = function(ovr) {
+let drawBorder = function(img) {
+ LOG("drawBorder", isQuiet());
+ if (img) ovr=img;
if (Bangle.isLocked())
ovr.setColor(ovr.theme.fgH);
else
@@ -170,7 +175,6 @@ let drawBorder = function(ovr) {
ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1);
ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2);
show(ovr);
- if (!isQuiet()) Bangle.setLCDPower(1);
};
let showCall = function(ovr, msg) {
@@ -232,13 +236,6 @@ let next = function(ovr) {
showMessage(ovr, eventQueue[0]);
};
-let showMapMessage = function(ovr, msg) {
- ovr.clearRect(2,2,ovr.getWidth()-3,ovr.getHeight()-3);
- drawMessage(ovr, {
- body: "Not implemented!"
- });
-};
-
let callBuzzTimer = null;
let stopCallBuzz = function() {
if (callBuzzTimer) {
@@ -407,7 +404,7 @@ let main = function(ovr, event) {
if (!lockListener) {
lockListener = function (){
- drawBorder(ovr);
+ drawBorder();
};
Bangle.on('lock', lockListener);
}
@@ -432,15 +429,22 @@ let main = function(ovr, event) {
let ovr;
exports.message = function(type, event) {
+ LOG("Got message", type, event);
// only handle some event types
if(!(type=="text" || type == "call")) return;
if(type=="text" && event.id == "nav") return;
if(event.handled) return;
bpp = 4;
- if (process.memory().free < 2000) bpp = 1;
+ if (process.memory().free < LOW_MEM)
+ bpp = 1;
- if (!ovr) {
+ while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){
+ let dropped = eventQueue.pop();
+ print("Dropped message because of memory constraints", dropped);
+ }
+
+ if (!ovr || ovr.getBPP() != bpp) {
ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, {
msb: true
});
@@ -456,6 +460,7 @@ exports.message = function(type, event) {
ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 };
main(ovr, event);
+ if (!isQuiet()) Bangle.setLCDPower(1);
event.handled = true;
g = _g;
};
diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json
index 500de5f66..c16a41f5c 100644
--- a/apps/messagesoverlay/metadata.json
+++ b/apps/messagesoverlay/metadata.json
@@ -1,7 +1,7 @@
{
"id": "messagesoverlay",
"name": "Messages Overlay",
- "version": "0.04",
+ "version": "0.06",
"description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)",
"icon": "app.png",
"type": "bootloader",
diff --git a/apps/miclock2/ChangeLog b/apps/miclock2/ChangeLog
index 534332e63..c34ba135c 100644
--- a/apps/miclock2/ChangeLog
+++ b/apps/miclock2/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Redraw only when seconds change
0.03: Fix typo in redraw check
+0.04: Register as clock and implement fast loading
diff --git a/apps/miclock2/clock-mixed.js b/apps/miclock2/clock-mixed.js
index bb1537313..e6bc0c094 100644
--- a/apps/miclock2/clock-mixed.js
+++ b/apps/miclock2/clock-mixed.js
@@ -1,29 +1,30 @@
// Code based on the original Mixed Clock
+{
/* jshint esversion: 6 */
-var locale = require("locale");
+const locale = require("locale");
const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 };
const Center = { "x": 120, "y": 96 };
const Widths = { hour: 2, minute: 2 };
-var buf = Graphics.createArrayBuffer(240,192,1,{msb:true});
-var lastDate = new Date();
+const buf = Graphics.createArrayBuffer(240,192,1,{msb:true});
+let timeoutId;
-function rotatePoint(x, y, d) {
- rad = -1 * d / 180 * Math.PI;
- var sin = Math.sin(rad);
- var cos = Math.cos(rad);
- xn = ((Center.x + x * cos - y * sin) + 0.5) | 0;
- yn = ((Center.y + x * sin - y * cos) + 0.5) | 0;
- p = [xn, yn];
- return p;
-}
+const rotatePoint = function(x, y, d, center, res) {
+ "jit";
+ const rad = -1 * d / 180 * Math.PI;
+ const sin = Math.sin(rad);
+ const cos = Math.cos(rad);
+ res[0] = ((center.x + x * cos - y * sin) + 0.5) | 0;
+ res[1] = ((center.y + x * sin - y * cos) + 0.5) | 0;
+};
// from https://github.com/espruino/Espruino/issues/1702
-function setLineWidth(x1, y1, x2, y2, lw) {
- var dx = x2 - x1;
- var dy = y2 - y1;
- var d = Math.sqrt(dx * dx + dy * dy);
+const setLineWidth = function(x1, y1, x2, y2, lw) {
+ "ram";
+ let dx = x2 - x1;
+ let dy = y2 - y1;
+ let d = Math.sqrt(dx * dx + dy * dy);
dx = dx * lw / d;
dy = dy * lw / d;
@@ -44,71 +45,84 @@ function setLineWidth(x1, y1, x2, y2, lw) {
x2 - dy, y2 + dx,
x1 - dy, y1 + dx
];
-}
+};
-function drawMixedClock(force) {
- var date = new Date();
- if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() !== lastDate.getSeconds()) {
- lastDate = date;
- var dateArray = date.toString().split(" ");
- var isEn = locale.name.startsWith("en");
- var point = [];
- var minute = date.getMinutes();
- var hour = date.getHours();
- var radius;
-
- g.reset();
- buf.clear();
-
- // draw date
- buf.setFont("6x8", 2);
- buf.setFontAlign(-1, 0);
- buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
- buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
- buf.setFontAlign(1, 0);
- buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
- buf.drawString(dateArray[3], 237, 176, true);
+const drawMixedClock = function() {
+ const date = new Date();
+ const dateArray = date.toString().split(" ");
+ const isEn = locale.name.startsWith("en");
+ let point = [0, 0];
+ const minute = date.getMinutes();
+ const hour = date.getHours();
+ let radius;
- // draw hour and minute dots
- for (i = 0; i < 60; i++) {
- radius = (i % 5) ? 2 : 4;
- point = rotatePoint(0, Radius.dots, i * 6);
- buf.fillCircle(point[0], point[1], radius);
- }
+ g.reset();
+ buf.clear();
- // draw digital time
- buf.setFont("6x8", 3);
- buf.setFontAlign(0, 0);
- buf.drawString(dateArray[4], 120, 120, true);
+ // draw date
+ buf.setFont("6x8", 2);
+ buf.setFontAlign(-1, 0);
+ buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
+ buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
+ buf.setFontAlign(1, 0);
+ buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
+ buf.drawString(dateArray[3], 237, 176, true);
- // draw new minute hand
- point = rotatePoint(0, Radius.min, minute * 6);
- buf.drawLine(Center.x, Center.y, point[0], point[1]);
- buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute));
- // draw new hour hand
- point = rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0);
- buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
-
- // draw center
- buf.fillCircle(Center.x, Center.y, Radius.center);
-
- g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
+ // draw hour and minute dots
+ for (i = 0; i < 60; i++) {
+ radius = (i % 5) ? 2 : 4;
+ rotatePoint(0, Radius.dots, i * 6, Center, point);
+ buf.fillCircle(point[0], point[1], radius);
}
-}
-Bangle.on('lcdPower', function(on) {
- if (on)
- drawMixedClock(true);
- Bangle.drawWidgets();
-});
+ // draw digital time
+ buf.setFont("6x8", 3);
+ buf.setFontAlign(0, 0);
+ buf.drawString(dateArray[4], 120, 120, true);
-setInterval(() => drawMixedClock(true), 30000); // force an update every 30s even screen is off
+ // draw new minute hand
+ rotatePoint(0, Radius.min, minute * 6, Center, point);
+ buf.drawLine(Center.x, Center.y, point[0], point[1]);
+ buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute));
+ // draw new hour hand
+ rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0, Center, point);
+ buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
+
+ // draw center
+ buf.fillCircle(Center.x, Center.y, Radius.center);
+
+ g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
+
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ const period = (Bangle.isLCDOn() ? 1000 : 60000); // Update every second if display is on else every minute
+ let timeout = period - (Date.now() % period);
+ timeoutId = setTimeout(()=>{
+ timeoutId = undefined;
+ drawMixedClock();
+ }, timeout);
+};
+
+const onLCDPower = function(on) {
+ if (on) {
+ drawMixedClock();
+ Bangle.drawWidgets();
+ }
+};
+Bangle.on('lcdPower', onLCDPower);
+
+Bangle.setUI({mode:"clock", remove:function() {
+ if (timeoutId !== undefined) {
+ delete buf.buffer;
+ clearTimeout(timeoutId);
+ timeoutId = undefined;
+ Bangle.removeListener('lcdPower',onLCDPower);
+ }
+}});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawMixedClock(); // immediately draw
-setInterval(drawMixedClock, 500); // update twice a second
-
-// Show launcher when middle button pressed after freeing memory first
-setWatch(() => {delete buf.buffer; Bangle.showLauncher()}, BTN2, {repeat:false,edge:"falling"});
+}
diff --git a/apps/miclock2/metadata.json b/apps/miclock2/metadata.json
index 094d0995a..f177ab4c1 100644
--- a/apps/miclock2/metadata.json
+++ b/apps/miclock2/metadata.json
@@ -1,7 +1,7 @@
{
"id": "miclock2",
"name": "Mixed Clock 2",
- "version": "0.03",
+ "version": "0.04",
"description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.",
"icon": "clock-mixed.png",
"type": "clock",
diff --git a/apps/multitimer/ChangeLog b/apps/multitimer/ChangeLog
index 27cfd17a5..67b0cc014 100644
--- a/apps/multitimer/ChangeLog
+++ b/apps/multitimer/ChangeLog
@@ -3,3 +3,6 @@
0.03: Use default Bangle formatter for booleans
0.04: Remove copied sched alarm.js & import newer features (oneshot alarms)
0.05: Fix creating new alarms/timers in hardmode
+0.06: Support fastloading
+0.07: Fix fastloading support - ensure drag handler's restored after
+ menu display/fastload removes it
diff --git a/apps/multitimer/app.js b/apps/multitimer/app.js
index 677b971ef..965a12d26 100644
--- a/apps/multitimer/app.js
+++ b/apps/multitimer/app.js
@@ -1,11 +1,12 @@
+{
Bangle.loadWidgets();
Bangle.drawWidgets();
-var R = Bangle.appRect;
-var layer;
-var drag;
-var timerInt1 = [];
-var timerInt2 = [];
+const R = Bangle.appRect;
+let layer;
+let drag;
+let timerInt1 = [];
+let timerInt2 = [];
function getCurrentTime() {
let time = new Date();
@@ -66,8 +67,7 @@ function setHM(alarm, on) {
function drawTimers() {
layer = 0;
- var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer");
- var alarms = require("sched").getAlarms();
+ const timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer");
function updateTimers(idx) {
if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() {
@@ -78,15 +78,15 @@ function drawTimers() {
}, 1000 - (timers[idx].t % 1000));
}
- var s = E.showScroller({
+ E.showScroller({
h : 40, c : timers.length+2,
back : function() {load();},
draw : (idx, r) => {
function drawMenuItem(a) {
+ let msg = "";
g.setClipRect(R.x,R.y,R.x2,R.y2);
if (idx > 0 && timers[idx-1].msg) msg = "\n"+(timers[idx-1].msg.length > 10 ?
timers[idx-1].msg.substring(0, 10)+"..." : timers[idx-1].msg);
- else msg = "";
return g.setColor(g.theme.bg2).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5})
.setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(a+msg,r.x+12,r.y+(r.h/2));
}
@@ -112,22 +112,22 @@ function drawTimers() {
else if (idx > 0 && idx < timers.length+1) timerMenu(idx-1);
}
});
+ setUI();
}
function timerMenu(idx) {
layer = -1;
- var timers = require("sched").getAlarms();
- var timerIdx = [];
- var j = 0;
+ const timers = require("sched").getAlarms();
+ const timerIdx = [];
+ let a;
+
for (let i = 0; i < timers.length; i++) {
if (timers[i].timer && timers[i].appid == "multitimer") {
a = i;
timerIdx.push(a);
- j++;
}
}
- var a = timers[timerIdx[idx]];
- var msg = "";
+ a = timers[timerIdx[idx]];
function updateTimer() {
if (timerInt1[0] == undefined) timerInt1[0] = setTimeout(function() {
@@ -138,7 +138,7 @@ function timerMenu(idx) {
}, 1000 - (a.t % 1000));
}
- var s = E.showScroller({
+ E.showScroller({
h : 40, c : 5,
back : function() {
clearInt();
@@ -153,6 +153,7 @@ function timerMenu(idx) {
}
if (i == 0) {
+ let msg = "";
if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg);
if (a.on == true) {
drawMenuItem(formatTime(a.t-getCurrentTime())+msg);
@@ -216,19 +217,17 @@ function timerMenu(idx) {
}
}
});
+ setUI();
}
function editTimer(idx, a) {
layer = -1;
- var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer");
- var alarms = require("sched").getAlarms();
- var timerIdx = [];
- var j = 0;
+ const timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer");
+ const alarms = require("sched").getAlarms();
+ const timerIdx = [];
for (let i = 0; i < alarms.length; i++) {
if (alarms[i].timer && alarms[i].appid == "multitimer") {
- b = i;
- timerIdx.push(b);
- j++;
+ timerIdx.push(i);
}
}
if (!a) {
@@ -238,9 +237,10 @@ function editTimer(idx, a) {
if (!a.data) {
a.data = { hm: false };
}
- var t = decodeTime(a.timer);
+ const t = decodeTime(a.timer);
function editMsg(idx, a) {
+ let msg;
g.clear();
idx < 0 ? msg = "" : msg = a.msg;
require("textinput").input({text:msg}).then(result => {
@@ -256,9 +256,10 @@ function editTimer(idx, a) {
E.showAlert("Must install keyboard app").then(function() {
editTimer(idx, a);
});
+ setUI();
}
- var menu = {
+ const menu = {
"": { "title": "Timer" },
"< Back": () => {
a.t = getCurrentTime() + a.timer;
@@ -312,7 +313,7 @@ function editTimer(idx, a) {
value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg,
//menu glitch? setTimeout required here
onchange: () => {
- var kbapp = require("Storage").read("textinput");
+ const kbapp = require("Storage").read("textinput");
if (kbapp != undefined) setTimeout(editMsg, 0, idx, a);
else setTimeout(kbAlert, 0);
}
@@ -324,11 +325,12 @@ function editTimer(idx, a) {
};
E.showMenu(menu);
+ setUI();
}
function drawSw() {
layer = 1;
- var sw = require("Storage").readJSON("multitimer.json", true) || [];
+ const sw = require("Storage").readJSON("multitimer.json", true) || [];
function updateTimers(idx) {
if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() {
@@ -339,12 +341,13 @@ function drawSw() {
}, 1000 - (sw[idx].t % 1000));
}
- var s = E.showScroller({
+ E.showScroller({
h : 40, c : sw.length+2,
back : function() {load();},
draw : (idx, r) => {
function drawMenuItem(a) {
+ let msg;
g.setClipRect(R.x,R.y,R.x2,R.y2);
if (idx > 0 && sw[idx-1].msg) msg = "\n"+(sw[idx-1].msg.length > 10 ?
sw[idx-1].msg.substring(0, 10)+"..." : sw[idx-1].msg);
@@ -374,11 +377,12 @@ function drawSw() {
else if (idx > 0 && idx < sw.length+1) swMenu(idx-1);
}
});
+ setUI();
}
function swMenu(idx, a) {
layer = -1;
- var sw = require("Storage").readJSON("multitimer.json", true) || [];
+ const sw = require("Storage").readJSON("multitimer.json", true) || [];
if (sw[idx]) a = sw[idx];
else {
a = {"t" : 0, "on" : false, "msg" : ""};
@@ -397,7 +401,7 @@ function swMenu(idx, a) {
function editMsg(idx, a) {
g.clear();
- msg = a.msg;
+ const msg = a.msg;
require("textinput").input({text:msg}).then(result => {
if (result != "") {
a.msg = result;
@@ -413,9 +417,10 @@ function swMenu(idx, a) {
E.showAlert("Must install keyboard app").then(function() {
swMenu(idx, a);
});
+ setUI();
}
- var s = E.showScroller({
+ E.showScroller({
h : 40, c : 5,
back : function() {
clearInt();
@@ -430,6 +435,7 @@ function swMenu(idx, a) {
}
if (i == 0) {
+ let msg;
if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg);
else msg = "";
if (a.on == true) {
@@ -482,7 +488,7 @@ function swMenu(idx, a) {
//edit message
if (i == 3) {
clearInt();
- var kbapp = require("Storage").read("textinput");
+ const kbapp = require("Storage").read("textinput");
if (kbapp != undefined) editMsg(idx, a);
else kbAlert();
}
@@ -495,21 +501,22 @@ function swMenu(idx, a) {
}
}
});
+ setUI();
}
function drawAlarms() {
layer = 2;
- var alarms = require("sched").getAlarms().filter(a => !a.timer);
+ const alarms = require("sched").getAlarms().filter(a => !a.timer);
- var s = E.showScroller({
+ E.showScroller({
h : 40, c : alarms.length+2,
back : function() {load();},
draw : (idx, r) => {
function drawMenuItem(a) {
g.setClipRect(R.x,R.y,R.x2,R.y2);
- var on = "";
- var dow = "";
+ let on = "";
+ let dow = "";
if (idx > 0 && alarms[idx-1].on == true) on = " - on";
else if (idx > 0 && alarms[idx-1].on == false) on = " - off";
if (idx > 0 && idx < alarms.length+1) dow = "\n"+"SMTWTFS".split("").map((d,n)=>alarms[idx-1].dow&(1<",r.x+(r.w/2),r.y+(r.h/2));
}
else if (idx > 0 && idx < alarms.length+1){
- var str = formatTime(alarms[idx-1].t);
+ const str = formatTime(alarms[idx-1].t);
drawMenuItem(str.slice(0, -3));
}
},
@@ -535,6 +542,7 @@ function drawAlarms() {
else if (idx > 0 && idx < alarms.length+1) editAlarm(idx-1);
}
});
+ setUI();
}
function editDOW(dow, onchange) {
@@ -542,26 +550,24 @@ function editDOW(dow, onchange) {
'': { 'title': 'Days of Week' },
'< Back' : () => onchange(dow)
};
- for (var i = 0; i < 7; i++) (i => {
- var dayOfWeek = require("locale").dow({ getDay: () => i });
+ for (let i = 0; i < 7; i++) (i => {
+ const dayOfWeek = require("locale").dow({ getDay: () => i });
menu[dayOfWeek] = {
value: !!(dow&(1< v ? dow |= 1< {
@@ -589,9 +596,10 @@ function editAlarm(idx, a) {
E.showAlert("Must install keyboard app").then(function() {
editAlarm(idx, a);
});
+ setUI();
}
- var menu = {
+ const menu = {
"": { "title": "Alarm" },
"< Back": () => {
if (idx >= 0) alarms[alarmIdx[idx]] = a;
@@ -646,7 +654,7 @@ function editAlarm(idx, a) {
value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg,
//menu glitch? setTimeout required here
onchange: () => {
- var kbapp = require("Storage").read("textinput");
+ const kbapp = require("Storage").read("textinput");
if (kbapp != undefined) setTimeout(editMsg, 0, idx, a);
else setTimeout(kbAlert, 0);
}
@@ -662,11 +670,21 @@ function editAlarm(idx, a) {
};
E.showMenu(menu);
+ setUI();
}
-drawTimers();
+function setUI() {
+ // E.showMenu/E.showScroller/E.showAlert call setUI, so we register onDrag() separately
+ // and tack on uiRemove after the fact to avoid interfering
+ Bangle.on("drag", onDrag);
+ Bangle.uiRemove = () => {
+ Bangle.removeListener("drag", onDrag);
+ Object.values(timerInt1).forEach(clearTimeout);
+ Object.values(timerInt2).forEach(clearTimeout);
+ };
+}
-Bangle.on("drag", e=>{
+function onDrag(e) {
if (layer < 0) return;
if (!drag) { // start dragging
drag = {x: e.x, y: e.y};
@@ -687,4 +705,7 @@ Bangle.on("drag", e=>{
else if (layer == 2) drawAlarms();
}
}
-});
+}
+
+drawTimers();
+}
diff --git a/apps/multitimer/metadata.json b/apps/multitimer/metadata.json
index 1db3235a1..e753d0581 100644
--- a/apps/multitimer/metadata.json
+++ b/apps/multitimer/metadata.json
@@ -1,7 +1,7 @@
{
"id": "multitimer",
"name": "Multi Timer",
- "version": "0.05",
+ "version": "0.07",
"description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.",
"icon": "app.png",
"screenshots": [
diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog
index 6b5e8172b..7d51a1d0c 100644
--- a/apps/openstmap/ChangeLog
+++ b/apps/openstmap/ChangeLog
@@ -16,11 +16,16 @@
Support for zooming in on map
Satellite count moved to widget bar to leave more room for the map
0.15: Make track drawing an option (default off)
-0.16: Draw waypoints, too.
+0.16: Draw waypoints, too
0.17: With new Recorder app allow track to be drawn in the background
Switch tile layer URL for faster/more reliable map tiles
0.18: Prefer map with highest resolution
0.19: Remember latitude, longitude & scale
0.20: Make Satellite counter widget 24px wide (was 48)
Move 'Center GPS' to the top of the menu
- If 'Recorder' app installed, add a 'Record' menu item
\ No newline at end of file
+ If 'Recorder' app installed, add a 'Record' menu item
+0.21: Draw a current position marker (Bangle.js 2 only)
+ Enable/Disable previous position marker in new setting "Draw cont. position"
+0.22: Replace position marker with direction arrow
+0.23: Bugfix: Enable Compass if needed
+0.24: Allow zooming by clicking the screen
diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md
index bf247c7b7..e0fc30abd 100644
--- a/apps/openstmap/README.md
+++ b/apps/openstmap/README.md
@@ -29,6 +29,7 @@ and marks the path that you've been travelling (if enabled), and
displays waypoints in the watch (if dependencies exist).
* Drag on the screen to move the map
+* Click bottom left to zoom in, bottom right to zoom out
* Press the button to bring up a menu, where you can zoom, go to GPS location,
put the map back in its default location, or choose whether to draw the currently
recording GPS track (from the `Recorder` app).
diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js
index 2afe56751..9b53077ab 100644
--- a/apps/openstmap/app.js
+++ b/apps/openstmap/app.js
@@ -7,6 +7,15 @@ var hasScrolled = false;
var settings = require("Storage").readJSON("openstmap.json",1)||{};
var plotTrack;
let checkMapPos = false; // Do we need to check the if the coordinates we have are valid
+var startDrag = 0;
+
+if (Bangle.setLCDOverlay) {
+ // Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted
+ var imgLoc = require("heatshrink").decompress(atob("jEYwINLAQk8AQl+AQn/AQcB/+AAQUD//AAQUH//gAQUP//wAQUf//4j8AvA9IA=="));
+ // overlay buffer for current location, a bit bigger then image so we can rotate
+ const ovSize = Math.ceil(Math.sqrt(imgLoc[0]*imgLoc[0]+imgLoc[1]*imgLoc[1]));
+ var ovLoc = Graphics.createArrayBuffer(ovSize,ovSize,imgLoc[2] & 0x7f,{msb:true});
+}
if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) {
// restore last view
@@ -15,6 +24,9 @@ if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !
m.scale = settings.scale;
checkMapPos = true;
}
+if (settings.dirSrc === undefined) {
+ settings.dirSrc = 1; // Default=GPS
+}
// Redraw the whole page
function redraw() {
@@ -27,8 +39,10 @@ function redraw() {
m.scale = m.map.scale;
m.draw();
}
+ checkMapPos = false;
drawPOI();
drawMarker();
+ drawLocation();
// if track drawing is enabled...
if (settings.drawTrack) {
if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
@@ -65,20 +79,63 @@ function drawPOI() {
})
}
-// Draw the marker for where we are
+function isInside(rect, e, w, h) {
+ return e.x-w/2>=rect.x && e.x+w/2=rect.y && e.y+h/2<=rect.y+rect.h;
+}
+
+// Draw the location & direction marker for where we are
function drawMarker() {
- if (!fix.fix) return;
+ if (!fix.fix || !settings.drawMarker) return;
var p = m.latLonToXY(fix.lat, fix.lon);
- g.setColor(1,0,0);
- g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
+ if (isInside(R, p, 4, 4)) { // avoid drawing over widget area
+ g.setColor(1,0,0);
+ g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
+ }
+}
+
+// Draw current location+direction with LCD Overlay (Bangle.js 2 only)
+function drawLocation() {
+ if (!Bangle.setLCDOverlay) {
+ return; // Overlay not supported
+ }
+
+ if (!fix.fix || !mapVisible || settings.dirSrc === 0) {
+ if (this.hasOverlay) {
+ Bangle.setLCDOverlay(); // clear if map is not visible or no fix
+ this.hasOverlay = false;
+ }
+ return;
+ }
+
+ var p = m.latLonToXY(fix.lat, fix.lon);
+
+ ovLoc.clear();
+ if (isInside(R, p, ovLoc.getWidth(), ovLoc.getHeight())) { // avoid drawing over widget area
+ const angle = settings.dirSrc === 1 ? fix.course : Bangle.getCompass().heading;
+ if (!isNaN(angle)) {
+ ovLoc.drawImage(imgLoc, ovLoc.getWidth()/2, ovLoc.getHeight()/2, {rotate: angle*Math.PI/180});
+ }
+ }
+ Bangle.setLCDOverlay({width:ovLoc.getWidth(), height:ovLoc.getHeight(),
+ bpp:ovLoc.getBPP(), transparent:0,
+ palette:new Uint16Array([0, g.toColor("#00F")]),
+ buffer:ovLoc.buffer
+ }, p.x-ovLoc.getWidth()/2, p.y-ovLoc.getHeight()/2);
+
+ this.hasOverlay = true;
}
Bangle.on('GPS',function(f) {
fix=f;
if (HASWIDGETS && WIDGETS["sats"]) WIDGETS["sats"].draw(WIDGETS["sats"]);
- if (mapVisible) drawMarker();
+ if (mapVisible) {
+ drawMarker();
+ drawLocation();
+ }
});
Bangle.setGPSPower(1, "app");
+Bangle.setCompassPower(settings.dirSrc === 2, "openstmap");
if (HASWIDGETS) {
Bangle.loadWidgets();
@@ -105,6 +162,7 @@ function showMenu() {
if (plotTrack && plotTrack.stop)
plotTrack.stop();
mapVisible = false;
+ drawLocation();
var menu = {
"":{title:/*LANG*/"Map"},
"< Back": ()=> showMap(),
@@ -128,13 +186,36 @@ function showMenu() {
value : !!settings.drawTrack,
onchange : v => { settings.drawTrack=v; writeSettings(); }
},
- /*LANG*/"Center Map": () =>{
+ /*LANG*/"Draw cont. position": {
+ value : !!settings.drawMarker,
+ onchange : v => { settings.drawMarker=v; writeSettings(); }
+ },
+ });
+
+ if (Bangle.setLCDOverlay) {
+ menu[/*LANG*/"Direction source"] = {
+ value: settings.dirSrc,
+ min: 0, max: 2,
+ format: v => [/*LANG*/"None", /*LANG*/"GPS", /*LANG*/"Compass"][v],
+ onchange: v => {
+ settings.dirSrc = v;
+ Bangle.setCompassPower(settings.dirSrc === 2, "openstmap");
+ writeSettings();
+ }
+ };
+ menu[/*LANG*/"Reset compass"] = () => {
+ Bangle.resetCompass();
+ showMap();
+ };
+ }
+
+ menu[/*LANG*/"Center Map"] = () =>{
m.lat = m.map.lat;
m.lon = m.map.lon;
m.scale = m.map.scale;
showMap();
- }
- });
+ };
+
// If we have the recorder widget, add a menu item to start/stop recording
if (WIDGETS.recorder) {
menu[/*LANG*/"Record"] = {
@@ -145,6 +226,7 @@ function showMenu() {
}
};
}
+ menu[/*LANG*/"Exit"] = () => load();
E.showMenu(menu);
}
@@ -155,13 +237,28 @@ function showMap() {
Bangle.setUI({mode:"custom",drag:e=>{
if (plotTrack && plotTrack.stop) plotTrack.stop();
if (e.b) {
+ if (!startDrag)
+ startDrag = getTime();
g.setClipRect(R.x,R.y,R.x2,R.y2);
g.scroll(e.dx,e.dy);
m.scroll(e.dx,e.dy);
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
hasScrolled = true;
+ drawLocation();
} else if (hasScrolled) {
+ delta = getTime() - startDrag;
+ startDrag = 0;
hasScrolled = false;
+ if (delta < 0.2) {
+ if (e.y > g.getHeight() / 2) {
+ if (e.x < g.getWidth() / 2) {
+ m.scale /= 2;
+ } else {
+ m.scale *= 2;
+ }
+ }
+ g.reset().clearRect(R);
+ }
redraw();
}
}, btn: () => showMenu() });
diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json
index 0ece4397e..988a1414d 100644
--- a/apps/openstmap/metadata.json
+++ b/apps/openstmap/metadata.json
@@ -2,7 +2,7 @@
"id": "openstmap",
"name": "OpenStreetMap",
"shortName": "OpenStMap",
- "version": "0.20",
+ "version": "0.24",
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
"readme": "README.md",
"icon": "app.png",
diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog
index 88185f337..897d06c26 100644
--- a/apps/qmsched/ChangeLog
+++ b/apps/qmsched/ChangeLog
@@ -8,3 +8,4 @@
Changed time selection to 5-minute intervals
0.08: Support new Bangle.js 2 menu
0.09: Use default Bangle formatter for booleans
+0.10: Allow using theme files
diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md
index 660bda787..e55a92084 100644
--- a/apps/qmsched/README.md
+++ b/apps/qmsched/README.md
@@ -14,6 +14,6 @@ Automatically turn Quiet Mode on or off at set times, and display a widget when
Switch to dark theme during Quiet Mode.
* **NOTE**: This switches between the default "Dark BW" and "Light BW" themes, so custom theme settings will be lost.
-### LCD Settings:
+### Options:
-If set, these override the default LCD settings while Quiet Mode is active.
\ No newline at end of file
+If set, these override the default LCD and theme settings while Quiet Mode is active.
diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js
index da43dd7d6..023c247ee 100644
--- a/apps/qmsched/app.js
+++ b/apps/qmsched/app.js
@@ -4,7 +4,8 @@ Bangle.drawWidgets();
const modeNames = [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"];
const B2 = process.env.HWVERSION===2;
// load global settings
-let bSettings = require('Storage').readJSON('setting.json',true)||{};
+const STORAGE = require('Storage');
+let bSettings = STORAGE.readJSON('setting.json',true)||{};
let current = 0|bSettings.quiet;
delete bSettings; // we don't need any other global settings
@@ -17,8 +18,8 @@ delete bSettings; // we don't need any other global settings
* Save settings to qmsched.json
*/
function save() {
- require('Storage').writeJSON('qmsched.json', settings);
- eval(require('Storage').read('qmsched.boot.js')); // apply new schedules right away
+ STORAGE.writeJSON('qmsched.json', settings);
+ eval(STORAGE.read('qmsched.boot.js')); // apply new schedules right away
}
function get(key, def) {
return (key in settings) ? settings[key] : def;
@@ -37,12 +38,12 @@ let settings,
* Load settings file, check if we need to migrate old setting formats to new
*/
function loadSettings() {
- settings = require('Storage').readJSON("qmsched.json", true) || {};
+ settings = 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)||{};
+ STORAGE.erase("qmsched.json"); // need to erase old file, or Things Break, somehow...
+ let bOptions = STORAGE.readJSON('setting.json',true)||{};
settings = {
options: bOptions.qmOptions || {},
scheds: settings,
@@ -51,7 +52,7 @@ function loadSettings() {
save();
// and clean up qmOptions from global settings file
delete bOptions.qmOptions;
- require('Storage').writeJSON('setting.json',bOptions);
+ STORAGE.writeJSON('setting.json',bOptions);
}
// apply defaults
settings = Object.assign({
@@ -82,8 +83,7 @@ function formatTime(t) {
* Apply theme
*/
function applyTheme() {
- const theme = (require("Storage").readJSON("setting.json", 1) || {}).theme;
- if (theme && theme.dark===g.theme.dark) return; // already correct
+ const theme = (STORAGE.readJSON("setting.json", 1) || {}).theme;
g.theme = theme;
delete g.reset;
g._reset = g.reset;
@@ -95,6 +95,33 @@ function applyTheme() {
m.draw();
}
+/**
+ * This creates menu entries for setting themes. This code is lifted from the setting app.
+ * @returns
+ */
+function showThemeMenu(back, quiet){
+ const option = quiet ? "quietTheme" : "normalTheme";
+ function cl(x) { return g.setColor(x).getColor(); }
+ var themesMenu = {
+ '':{title:/*LANG*/'Theme', back: back},
+ /*LANG*/'Default': ()=>{
+ unset(option);
+ back();
+ }
+ };
+
+ STORAGE.list(/^.*\.theme$/).forEach(
+ n => {
+ let newTheme = STORAGE.readJSON(n);
+ themesMenu[newTheme.name ? newTheme.name : n] = () => {
+ set(option, n);
+ back();
+ };
+ }
+ );
+ E.showMenu(themesMenu);
+}
+
/**
* Library uses this to make the app update itself
* @param {int} mode New Quite Mode
@@ -109,8 +136,8 @@ function setAppQuietMode(mode) {
let m;
function showMainMenu() {
- let menu = {"": {"title": /*LANG*/"Quiet Mode"},};
- menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Exit"] = () => {load();};
+ let menu = {"": {"title": /*LANG*/"Quiet Mode"}};
+ menu[B2 ? "< Back" : /*LANG*/"< Exit"] = () => {load();};
menu[/*LANG*/"Current Mode"] = {
value: current,
min:0, max:2, wrap: true,
@@ -127,7 +154,7 @@ function showMainMenu() {
value: !!get("switchTheme"),
onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
};
- menu[/*LANG*/"LCD Settings"] = () => showOptionsMenu();
+ menu[/*LANG*/"Options"] = () => showOptionsMenu();
m = E.showMenu(menu);
}
@@ -142,7 +169,7 @@ function showEditMenu(index) {
mode = s.mode;
}
let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}};
- menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Cancel"] = () => showMainMenu();
+ menu[B2 ? "< Back" : /*LANG*/"< Cancel"] = () => showMainMenu();
menu[/*LANG*/"Hours"] = {
value: hrs,
min:0, max:23, wrap:true,
@@ -199,7 +226,7 @@ function showOptionsMenu() {
let resetTimeout;
const oMenu = {
"": {"title": /*LANG*/"LCD Settings"},
- /*LANG*/"< Back": () => showMainMenu(),
+ "< Back": () => showMainMenu(),
/*LANG*/"LCD Brightness": {
value: get("brightness", 0),
min: 0, // 0 = use default
@@ -251,6 +278,10 @@ function showOptionsMenu() {
onchange: () => {toggle("wakeOnTwist");},
},
};
+
+ oMenu[/*LANG*/"Normal Theme"] = () => showThemeMenu(showOptionsMenu, false);
+ oMenu[/*LANG*/"Quiet Theme"] = () => showThemeMenu(showOptionsMenu, true);
+
m = E.showMenu(oMenu);
}
diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js
index 9696657cc..1d636f263 100644
--- a/apps/qmsched/lib.js
+++ b/apps/qmsched/lib.js
@@ -3,26 +3,70 @@
* @param {int} mode Quiet Mode
*/
function switchTheme(mode) {
- if (!!mode === g.theme.dark) return; // nothing to do
- let s = require("Storage").readJSON("setting.json", 1) || {};
- // default themes, copied from settings.js:showThemeMenu()
function cl(x) { return g.setColor(x).getColor(); }
- s.theme = mode ? {
+ const readTheme = function(name) {
+ const n = require("Storage").readJSON(name);
+ return {
+ fg:cl(n.fg), bg:cl(n.bg),
+ fg2:cl(n.fg2), bg2:cl(n.bg2),
+ fgH:cl(n.fgH), bgH:cl(n.bgH),
+ dark:n.dark
+ };
+ }
+ const s = require("Storage").readJSON("setting.json", 1) || {};
+ // default themes, copied from settings.js:showThemeMenu()
+
+ const q = require("Storage").readJSON("qmsched.json", 1) || {};
+ let quietTheme = {
// 'Dark BW'
- fg: cl("#fff"), bg: cl("#000"),
- fg2: cl("#0ff"), bg2: cl("#000"),
- fgH: cl("#fff"), bgH: cl("#00f"),
- dark: true
- } : {
- // 'Light BW'
- fg: cl("#000"), bg: cl("#fff"),
- fg2: cl("#000"), bg2: cl("#cff"),
- fgH: cl("#000"), bgH: cl("#0ff"),
- dark: false
+ fg:cl("#fff"), bg:cl("#000"),
+ fg2:cl("#fff"), bg2:cl("#004"),
+ fgH:cl("#fff"), bgH:cl("#00f"),
+ dark:true
};
- require("Storage").writeJSON("setting.json", s);
- // reload clocks with new theme, otherwise just wait for user to switch apps
- if (Bangle.CLOCK) load(global.__FILE__);
+ let normalTheme = {
+ // 'Light BW'
+ fg:cl("#000"), bg:cl("#fff"),
+ fg2:cl("#000"), bg2:cl("#cff"),
+ fgH:cl("#000"), bgH:cl("#0ff"),
+ dark:false
+ };
+
+ let miss = false;
+
+ // ensure referenced theme files actually exist or remove reference
+ if (q.normalTheme && require("Storage").read(q.normalTheme) == undefined){
+ delete q.normalTheme;
+ miss = true;
+ }
+ if (q.quietTheme && require("Storage").read(q.quietTheme) == undefined){
+ delete q.quietTheme;
+ miss = true;
+ }
+ if (miss)
+ require("Storage").writeJSON("qmsched.json", q);
+
+ // load theme files
+ if (q.normalTheme)
+ normalTheme = readTheme(q.normalTheme);
+ if (q.quietTheme)
+ quietTheme = readTheme(q.quietTheme);
+
+
+ const newTheme = mode ? quietTheme : normalTheme;
+ let changed = false;
+ for (const c in newTheme) {
+ if (!(c in g.theme) || newTheme[c] !== g.theme[c]) {
+ changed = true;
+ break;
+ }
+ }
+ if (changed) {
+ s.theme = newTheme;
+ require("Storage").writeJSON("setting.json", s);
+ // reload clocks with new theme, otherwise just wait for user to switch apps
+ if (Bangle.CLOCK) load(global.__FILE__);
+ }
}
/**
* Apply LCD options and theme for given mode
diff --git a/apps/qmsched/metadata.json b/apps/qmsched/metadata.json
index 23bdbd2e4..26019b6ba 100644
--- a/apps/qmsched/metadata.json
+++ b/apps/qmsched/metadata.json
@@ -2,7 +2,7 @@
"id": "qmsched",
"name": "Quiet Mode Schedule and Widget",
"shortName": "Quiet Mode",
- "version": "0.09",
+ "version": "0.10",
"description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
"icon": "app.png",
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog
index 991b811cb..5f4d1f8b4 100644
--- a/apps/recorder/ChangeLog
+++ b/apps/recorder/ChangeLog
@@ -35,3 +35,4 @@
0.27: Fix first ever recorded filename being log0 (now all are dated)
0.28: Automatically create new track if the filename is different
0.29: When plotting with OpenStMap scale map to track width & height
+0.30: Add clock info for showing and toggling recording state
diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js
new file mode 100644
index 000000000..b4a9a45b8
--- /dev/null
+++ b/apps/recorder/clkinfo.js
@@ -0,0 +1,38 @@
+(function () {
+ const recimg = () =>
+ atob("GBiBAAAAABwAAD4MAH8eAH8OAH8AAD4QABx8AAD8AAH+AAE+AAM/AAN7wAN4wAB4AAB8AAD8AADOAAHGAAOHAAMDAAIBAAAAAAAAAA==");
+
+ const pauseimg = () =>
+ atob("GBiBAAAAAAAAAAAAAAAAAAHDgAPnwAPjwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPjwAPnwAHDgAAAAAAAAAAAAAAAAA==");
+
+ return {
+ name: "Bangle",
+ items: require("Storage").readJSON("recorder.json") ? [
+ {
+ name: "Toggle",
+ get: () => {
+ const w = WIDGETS && WIDGETS["recorder"];
+
+ return w && w.isRecording() ? {
+ text: "Recording",
+ short: "Rec",
+ img: recimg(),
+ } : {
+ text: w ? "Paused" : "No rec",
+ short: w ? "Paused" : "No rec",
+ img: pauseimg(),
+ };
+ },
+ run: () => {
+ const w = WIDGETS && WIDGETS["recorder"];
+ if(w){
+ Bangle.buzz();
+ w.setRecording(!w.isRecording(), { force: "append" });
+ }
+ },
+ show: () => {},
+ hide: () => {},
+ },
+ ] : [],
+ };
+});
diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json
index e714abf8d..15ba165d1 100644
--- a/apps/recorder/metadata.json
+++ b/apps/recorder/metadata.json
@@ -2,10 +2,10 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
- "version": "0.29",
+ "version": "0.30",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
- "tags": "tool,outdoors,gps,widget",
+ "tags": "tool,outdoors,gps,widget,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
@@ -13,6 +13,7 @@
{"name":"recorder.app.js","url":"app.js"},
{"name":"recorder.img","url":"app-icon.js","evaluate":true},
{"name":"recorder.wid.js","url":"widget.js"},
+ {"name":"recorder.clkinfo.js","url":"clkinfo.js"},
{"name":"recorder.settings.js","url":"settings.js"}
],
"data": [
diff --git a/apps/rescalc/ChangeLog b/apps/rescalc/ChangeLog
index 7b1d6baca..21ff1f6e8 100644
--- a/apps/rescalc/ChangeLog
+++ b/apps/rescalc/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Fixes colors not matching user input from color menu in some cases, 3 bands are now shown larger, various code improvements.
0.03: Use transparent icon with better visibility on dark backgrounds, new resistor img with darker outlines
+0.04: Fix capitalization. Improve decimal handling.
diff --git a/apps/rescalc/app.js b/apps/rescalc/app.js
index 566809837..4debb6c5f 100644
--- a/apps/rescalc/app.js
+++ b/apps/rescalc/app.js
@@ -3,19 +3,19 @@
// https://icons8.com/icon/ISAVBnskZod0/resistor
let colorData = {
- black: { value: 0, multiplier: 1, hex: '#000' },
- brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' },
- red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' },
- orange: { value: 3, multiplier: 1000, hex: '#FF9900' },
- yellow: { value: 4, multiplier: 10000, hex: '#ff0' },
- green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' },
- blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' },
- violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' },
- grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' },
- white: { value: 9, multiplier: 1000000000, hex: '#fff' },
- gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' },
- silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' },
- none: { tolerance: 20 },
+ Black: { value: 0, multiplier: 1, hex: '#000' },
+ Brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' },
+ Red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' },
+ Orange: { value: 3, multiplier: 1000, hex: '#FF9900' },
+ Yellow: { value: 4, multiplier: 10000, hex: '#ff0' },
+ Green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' },
+ Blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' },
+ Violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' },
+ Grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' },
+ White: { value: 9, multiplier: 1000000000, hex: '#fff' },
+ Gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' },
+ Silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' },
+ None: { tolerance: 20 },
};
function clearScreen() { // Except Back Button
@@ -27,7 +27,7 @@ function colorBandsToResistance(colorBands) {
let firstBand = colorBands[0];
let secondBand = colorBands[1];
let multiplierBand = colorBands[2];
- let toleranceBand = colorBands[3] || 'none'; // Add a default value for toleranceBand
+ let toleranceBand = colorBands[3] || 'None'; // Add a default value for toleranceBand
let significantDigits = colorData[firstBand].value * 10 + colorData[secondBand].value;
let multiplier = colorData[multiplierBand].multiplier;
let resistance = significantDigits * multiplier;
@@ -35,57 +35,65 @@ function colorBandsToResistance(colorBands) {
return [resistance, tolerance];
}
+// Function to get color bands based on resistance and tolerance
function resistanceToColorBands(resistance, tolerance) {
let firstDigit, secondDigit, multiplier;
- if (resistance < 1) {
- // The resistance is less than 1, so we need to handle this case specially
- let count = 0;
- while (resistance < 1) {
- resistance *= 10;
- count++;
- }
- // Now, resistance is a whole number and count is how many times we had to multiply by 10
- let resistanceStr = resistance.toString();
- firstDigit = 0; // Set the first band color to be black
- secondDigit = Number(resistanceStr.charAt(0)); // Set the second band color to be the significant digit
- // Use count to determine the multiplier
- multiplier = count === 1 ? 0.1 : 0.01;
- } else {
- // Convert the resistance to a string so we can manipulate it easily
- let resistanceStr = resistance.toString();
- if (resistanceStr.length === 1) { // Check if resistance is a single digit
- firstDigit = 0;
- secondDigit = Number(resistanceStr.charAt(0));
- multiplier = 1; // Set multiplier to 1 for single digit resistance values
+ let resistanceStr = resistance.toString();
+ let decimalIndex = resistanceStr.indexOf('.');
+
+ // Handle resistance with decimal
+ if (decimalIndex !== -1) {
+ let integerDigits = resistanceStr.substring(0, decimalIndex);
+ let decimalDigits = resistanceStr.substring(decimalIndex + 1);
+ let leadingZeros = decimalDigits.match(/^0*/)[0].length;
+
+ // If resistance is less than 1
+ if (parseInt(integerDigits) === 0) {
+ if (leadingZeros === decimalDigits.length - 1) {
+ // If only one significant digit
+ firstDigit = 0;
+ secondDigit = parseInt(decimalDigits.charAt(leadingZeros));
+ multiplier = 1 / Math.pow(10, leadingZeros + 1);
+ } else {
+ // If more than one significant digit
+ firstDigit = parseInt(decimalDigits.charAt(leadingZeros));
+ secondDigit = parseInt(decimalDigits.charAt(leadingZeros + 1));
+ multiplier = 1 / Math.pow(10, leadingZeros + 2);
+ }
} else {
- // Extract the first two digits from the resistance value
- firstDigit = Number(resistanceStr.charAt(0));
- secondDigit = Number(resistanceStr.charAt(1));
- // Calculate the multiplier by matching it directly with the length of digits
- multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1);
+ // If resistance is greater than 1
+ firstDigit = parseInt(integerDigits.charAt(0));
+ secondDigit = parseInt(decimalDigits.charAt(0));
+ multiplier = 1 / Math.pow(10, decimalDigits.length);
}
+ } else {
+ // Handle resistance without decimal
+ firstDigit = resistanceStr.length === 1 ? 0 : parseInt(resistanceStr.charAt(0));
+ secondDigit = parseInt(resistanceStr.charAt(resistanceStr.length === 1 ? 0 : 1));
+ multiplier = Math.pow(10, resistanceStr.length - 2);
}
- let firstBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].value === firstDigit;
- });
- let firstBand = firstBandEntry ? firstBandEntry[1].hex : undefined;
- let secondBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].value === secondDigit;
- });
- let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined;
- let multiplierBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].multiplier === multiplier;
- });
- let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined;
- let toleranceBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].tolerance === tolerance;
- });
- let toleranceBand = toleranceBandEntry ? toleranceBandEntry[1].hex : undefined;
- let bands = [firstBand, secondBand, multiplierBand];
+
+ // Generate color bands array
+ let bands = [
+ getBandColor('value', firstDigit),
+ getBandColor('value', secondDigit),
+ getBandColor('multiplier', multiplier),
+ ];
+
+ // Add tolerance color band if provided
+ let toleranceBand = getBandColor('tolerance', tolerance);
if (toleranceBand) bands.push(toleranceBand);
return bands;
}
+// Helper function to get color band based on property and value
+function getBandColor(property, value) {
+ let entry = Object.entries(colorData).find(function (entry) {
+ return entry[1][property] === value;
+ });
+ return entry ? entry[1].hex : undefined;
+}
+
function drawResistor(colorBands, tolerance) {
let img = require("Storage").read("rescalc-resistor.img");
let resistorBodyWidth = 51;
@@ -208,17 +216,17 @@ function drawResistance(resistance, tolerance) {
// Populate colorBandMenu with colors from colorData
for (let color in colorData) {
if (bandNumber === 1 || bandNumber === 2) {
- if (color !== 'none' && color !== 'gold' && color !== 'silver') {
+ if (color !== 'None' && color !== 'Gold' && color !== 'Silver') {
(function (color) {
- colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
+ colorBandMenu[color] = function () {
setBandColor(bandNumber, color);
};
})(color);
}
} else if (bandNumber === 3) {
- if (color !== 'none') {
+ if (color !== 'None') {
(function (color) {
- colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
+ colorBandMenu[color] = function () {
setBandColor(bandNumber, color);
};
})(color);
@@ -226,7 +234,7 @@ function drawResistance(resistance, tolerance) {
} else if (bandNumber === 4) {
if (colorData[color].hasOwnProperty('tolerance')) {
(function (color) {
- colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
+ colorBandMenu[color] = function () {
setBandColor(bandNumber, color);
};
})(color);
@@ -407,7 +415,6 @@ function drawResistance(resistance, tolerance) {
};
function showResistanceEntryMenu() {
- // Update the 'Ohms' field
resistanceEntryMenu['Ohms'].value = settings.resistance;
resistanceEntryMenu['Ohms'].format = v => {
let multipliedValue = v * (settings.multiplier || 1);
diff --git a/apps/rescalc/metadata.json b/apps/rescalc/metadata.json
index 473f334d7..cd2b7ea31 100644
--- a/apps/rescalc/metadata.json
+++ b/apps/rescalc/metadata.json
@@ -3,7 +3,7 @@
"name": "Resistor Calculator",
"shortName": "Resistor Calc",
"icon": "rescalc.png",
- "version":"0.03",
+ "version":"0.04",
"screenshots": [
{"url": "screenshot.png"},
{"url": "screenshot-1.png"},
diff --git a/apps/sched/interface.html b/apps/sched/interface.html
index b67029fa2..cd2c9c595 100644
--- a/apps/sched/interface.html
+++ b/apps/sched/interface.html
@@ -334,7 +334,7 @@ function onInit() {
-
+
diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog
index 6d2f5d2b4..92088af2b 100644
--- a/apps/sensortools/ChangeLog
+++ b/apps/sensortools/ChangeLog
@@ -4,3 +4,4 @@
0.04: Correct type of time attribute in gps to Date
0.05: Fix gps emulation interpolation
Add setting for log output
+0.06: Fix sensortools breaking Bangle.emit with multiple arguments (e.g. "message")
diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js
index 5e1c199c2..fae856108 100644
--- a/apps/sensortools/lib.js
+++ b/apps/sensortools/lib.js
@@ -20,37 +20,44 @@ exports.enable = () => {
Bangle.sensortoolsOrigEmit = Bangle.emit;
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
+ const modifyArguments = function(args, value) {
+ if (args.length >= 1)
+ args[0] = value;
+ return args;
+ };
+
Bangle.on = function(name, callback) {
if (onEvents[name]) {
log("Redirecting listener for", name, "to", name + "_mod");
- Bangle.sensortoolsOrigOn(name + "_mod", callback);
- Bangle.sensortoolsOrigOn(name, (e) => {
- log("Redirected event for", name, "to", name + "_mod");
- Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
+ let origName = name;
+ Bangle.sensortoolsOrigOn(origName, (e) => {
+ log("Redirected event for", origName, "to", origName + "_mod");
+ Bangle.sensortoolsOrigEmit(origName + "_mod", onEvents[origName](e));
});
+ Bangle.sensortoolsOrigOn.apply(this, modifyArguments(arguments, name + "_mod"));
} else {
log("Pass through on call for", name, callback);
- Bangle.sensortoolsOrigOn(name, callback);
+ Bangle.sensortoolsOrigOn.apply(this, arguments);
}
};
- Bangle.removeListener = function(name, callback) {
+ Bangle.removeListener = function(name) {
if (onEvents[name]) {
log("Removing augmented listener for", name, onEvents[name]);
- Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
+ Bangle.sensortoolsOrigRemoveListener.apply(this, modifyArguments(arguments, name + "_mod"));
} else {
log("Pass through remove listener for", name);
- Bangle.sensortoolsOrigRemoveListener(name, callback);
+ Bangle.sensortoolsOrigRemoveListener.apply(this, arguments);
}
};
- Bangle.emit = function(name, event) {
+ Bangle.emit = function(name) {
if (onEvents[name]) {
log("Augmenting emit call for", name, onEvents[name]);
- Bangle.sensortoolsOrigEmit(name + "_mod", event);
+ Bangle.sensortoolsOrigEmit.apply(this, modifyArguments(arguments, name + "_mod"));
} else {
log("Pass through emit call for", name);
- Bangle.sensortoolsOrigEmit(name, event);
+ Bangle.sensortoolsOrigEmit.apply(this, arguments);
}
};
diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json
index bffffd090..23749d537 100644
--- a/apps/sensortools/metadata.json
+++ b/apps/sensortools/metadata.json
@@ -2,7 +2,7 @@
"id": "sensortools",
"name": "Sensor tools",
"shortName": "Sensor tools",
- "version": "0.05",
+ "version": "0.06",
"description": "Tools for testing and debugging apps that use sensor input",
"icon": "icon.png",
"type": "bootloader",
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index d090add58..942eee11d 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -68,3 +68,4 @@ of 'Select Clock'
0.60: Moved LCD calibration to top of menu, and use 12 taps (not 8)
LCD calibration will now error if the calibration is obviously wrong
0.61: Permit temporary bypass of the BLE whitelist
+0.62: Fix whitelist showing as 'on' by default when it's not after 0.59
\ No newline at end of file
diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json
index b2b19dd6b..5e0753dd2 100644
--- a/apps/setting/metadata.json
+++ b/apps/setting/metadata.json
@@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
- "version": "0.61",
+ "version": "0.62",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index d22f28412..87aaba9d1 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -193,7 +193,7 @@ function showBLEMenu() {
/*LANG*/'Whitelist': {
value:
(
- settings.whitelist_disabled ? /*LANG*/"off" : /*LANG*/"on"
+ (settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"off" : /*LANG*/"on"
) + (
settings.whitelist
? " (" + settings.whitelist.length + ")"
diff --git a/apps/sixths/ChangeLog b/apps/sixths/ChangeLog
new file mode 100644
index 000000000..263d4078d
--- /dev/null
+++ b/apps/sixths/ChangeLog
@@ -0,0 +1 @@
+0.01: attempt to import
diff --git a/apps/sixths/README.md b/apps/sixths/README.md
new file mode 100644
index 000000000..e5d76d5ad
--- /dev/null
+++ b/apps/sixths/README.md
@@ -0,0 +1,51 @@
+# Sixth Sense 
+
+Clock displaying just the right information at the right time.
+
+Experimental clock face. It aims to display just the right importation
+at the right time, with focus on various sensors. Normally, digital
+clock, date and step count in kilometers is displayed.
+
+It saves a lot of logs for debugging and future use. In particular, it
+saves battery and step counters all the time, and GPS positions
+whenever it is enabled. You may not want to use it if you are secret
+agent or journalist in Iran.
+
+Application can be controled by gestures, making control possible in
+challenging conditions such as on horseback. Gestures are based on
+morse code, left half of screen is for ".", right half of screen is
+for "-". Gesture should always start in the upper half of screen. If
+next symbol is same, drag vertically, else drag horizontally.
+
+Power saving GPS mode is available, suitable for hiking. GPS fix is
+acquired once every few minutes, and written into the log. Approximate
+distance travelled is displayed. Due to only taking fix every few
+minutes, real distance will be usually higher than approximation.
+
+Useful gestures:
+
+F -- disable GPS.
+G -- enable GPS for 4 hours.
+N -- take a note and write it to the log.
+
+When application detects watch is being worn, it will use vibrations
+to communicate back to the user.
+
+E -- acknowledge, gesture understood.
+T -- start of new hour.
+
+Written by: [Pavel Machek](https://github.com/pavelmachek)
+
+## Future Development
+
+I'd like to expand GPS development more, allowing marking of waypoints
+and navigating back to them. I'd also like to make power-saving
+optional.
+
+I'd also like to utilize the altimeter more, likely remembering
+altitude of home location, automatically correcting for pressure every
+night.
+
+I'd like to make display nicer, and likely more dynamic, displaying
+whatever application believes is most important at the time (and
+possibly allowing scrolling).
\ No newline at end of file
diff --git a/apps/sixths/app-icon.js b/apps/sixths/app-icon.js
new file mode 100644
index 000000000..c75930b47
--- /dev/null
+++ b/apps/sixths/app-icon.js
@@ -0,0 +1,2 @@
+require("heatshrink").decompress(atob("mEwgP/AEn3AgfvAonnAon3+/9AoX7+/5CwX7A4IWCB4P7/3/+YZC/gOD/4eDAAIeC/0An4PC+P/4Y3C5E/Cwcgj/+v4WB4EP/+fEoOAg//441BAoQjB84FCDwPvwED/5BB+4FCBYIFBCIJRB/fAAoPPBoIvCn41B+A7EnF//BHCHAODAoXwgF/N4aMCAog1BQoJ0Cv5oCCAO/UAP9AoPP+fv/oOBW4IFBDQP794FB/5BBMwIFBJoItD375eA"))
+
diff --git a/apps/sixths/app.js b/apps/sixths/app.js
new file mode 100644
index 000000000..ce036f79d
--- /dev/null
+++ b/apps/sixths/app.js
@@ -0,0 +1,508 @@
+const W = g.getWidth();
+const H = g.getHeight();
+
+var cx = 100; cy = 105; sc = 70;
+var buzz = "", msg = "";
+temp = 0; alt = 0; bpm = 0;
+var buzz = "", msg = "", inm = "", l = "", note = "(NOTEHERE)";
+var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note
+
+var gps_on = 0, last_fix = 0, last_restart = 0, last_pause = 0, last_fstart = 0; // utime
+var gps_needed = 0, gps_limit = 0; // seconds
+var prev_fix = null;
+var gps_dist = 0;
+
+var is_active = false;
+var cur_altitude = 0, cur_temperature = 0, alt_adjust = 0;
+const rest_altitude = 354;
+
+function toMorse(x) {
+ r = "";
+ for (var i = 0; i < x.length; i++) {
+ c = x[i];
+ if (c == " ") {
+ r += " ";
+ continue;
+ }
+ r += asciiToMorse(c) + " ";
+ }
+ return r;
+}
+
+function aload(s) {
+ buzz += toMorse(' E');
+ load(s);
+}
+
+function gpsRestart() {
+ print("gpsRestart");
+ Bangle.setGPSPower(1, "sixths");
+ last_restart = getTime();
+ last_pause = 0;
+ last_fstart = 0;
+}
+
+function gpsPause() {
+ print("gpsPause");
+ Bangle.setGPSPower(0, "sixths");
+ last_restart = 0;
+ last_pause = getTime();
+}
+
+function gpsOn() {
+ gps_on = getTime();
+ gps_needed = 1000;
+ gps_limit = 60*60*4;
+ last_fix = 0;
+ prev_fix = null;
+ gps_dist = 0;
+ gpsRestart();
+}
+
+function gpsOff() {
+ Bangle.setGPSPower(0, "sixths");
+ gps_on = 0;
+}
+
+function inputHandler(s) {
+ print("Ascii: ", s);
+ if (mode == 1) {
+ note = note + s;
+ mode_time = getTime();
+ return;
+ }
+ switch(s) {
+ case 'B':
+ s = ' B';
+ bat = E.getBattery();
+ if (bat > 45)
+ s += 'E';
+ else
+ s = s+(bat/5);
+ buzz += toMorse(s);
+ break;
+ case 'F': gpsOff(); break;
+ case 'G': gpsOn(); break;
+ case 'L': aload("altimeter.app.js"); break;
+ case 'N': mode = 1; note = ">"; mode_time = getTime(); break;
+ case 'O': aload("orloj.app.js"); break;
+ case 'T':
+ s = ' T';
+ d = new Date();
+ s += d.getHours() % 10;
+ s += add0(d.getMinutes());
+ buzz += toMorse(s);
+ break;
+ case 'R': aload("run.app.js"); break;
+ }
+}
+
+const morseDict = {
+ '.-': 'A',
+ '-...': 'B',
+ '-.-.': 'C',
+ '-..': 'D',
+ '.': 'E',
+ '..-.': 'F',
+ '--.': 'G',
+ '....': 'H',
+ '..': 'I',
+ '.---': 'J',
+ '-.-': 'K',
+ '.-..': 'L',
+ '--': 'M',
+ '-.': 'N',
+ '---': 'O',
+ '.--.': 'P',
+ '--.-': 'Q',
+ '.-.': 'R',
+ '...': 'S',
+ '-': 'T',
+ '..-': 'U',
+ '...-': 'V',
+ '.--': 'W',
+ '-..-': 'X',
+ '-.--': 'Y',
+ '--..': 'Z',
+ '.----': '1',
+ '..---': '2',
+ '...--': '3',
+ '....-': '4',
+ '.....': '5',
+ '----.': '9',
+ '---..': '8',
+ '--...': '7',
+ '-....': '6',
+ '-----': '0',
+ };
+
+let asciiDict = {};
+
+for (let k in morseDict) {
+ print(k, morseDict[k]);
+ asciiDict[morseDict[k]] = k;
+}
+
+
+function morseToAscii(morse) {
+ return morseDict[morse];
+}
+
+function asciiToMorse(char) {
+ return asciiDict[char];
+}
+
+function morseHandler() {
+ inputHandler(morseToAscii(inm));
+ inm = "";
+ l = "";
+}
+
+function touchHandler(d) {
+ let x = Math.floor(d.x);
+ let y = Math.floor(d.y);
+
+ g.setColor(0.25, 0, 0);
+ g.fillCircle(W-x, W-y, 5);
+
+ if (d.b) {
+ if (x < W/2 && y < H/2 && l != ".u") {
+ inm = inm + ".";
+ l = ".u";
+ }
+ if (x > W/2 && y < H/2 && l != "-u") {
+ inm = inm + "-";
+ l = "-u";
+ }
+ if (x < W/2 && y > H/2 && l != ".d") {
+ inm = inm + ".";
+ l = ".d";
+ }
+ if (x > W/2 && y > H/2 && l != "-d") {
+ inm = inm + "-";
+ l = "-d";
+ }
+
+ } else
+ morseHandler();
+
+ print(inm, "drag:", d);
+}
+
+function add0(i) {
+ if (i > 9) {
+ return ""+i;
+ } else {
+ return "0"+i;
+ }
+}
+
+var lastHour = -1, lastMin = -1;
+
+function logstamp(s) {
+ logfile.write("utime=" + getTime() + " " + s + "\n");
+}
+
+function loggps(fix) {
+ logfile.write(fix.lat + " " + fix.lon + " ");
+ logstamp("");
+}
+
+function hourly() {
+ print("hourly");
+ s = ' T';
+ if (is_active)
+ buzz += toMorse(s);
+ logstamp("");
+}
+
+function fivemin() {
+ print("fivemin");
+ s = ' B';
+ bat = E.getBattery();
+ if (bat < 45) {
+ s = s+(bat/5);
+ if (is_active)
+ buzz += toMorse(s);
+ }
+ if (0)
+ Bangle.getPressure().then((x) => { cur_altitude = x.altitude;
+ cur_temperature = x.temperature; },
+ print)
+ .catch(print);
+}
+
+function every(now) {
+ if ((mode > 0) && (mode_time - getTime() > 60)) {
+ if (mode == 1) {
+ logstamp(">" + note);
+ }
+ mode = 0;
+ }
+ if (gps_on && getTime() - gps_on > gps_limit) {
+ Bangle.setGPSPower(0, "sixths");
+ gps_on = 0;
+ }
+
+ if (lastHour != now.getHours()) {
+ lastHour = now.getHours();
+ hourly();
+ }
+ if (lastMin / 5 != now.getMinutes() / 5) {
+ lastMin = now.getMinutes();
+ fivemin();
+ }
+
+}
+
+// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
+// https://www.movable-type.co.uk/scripts/latlong.html
+// (Equirectangular approximation)
+function calcDistance(a,b) {
+ function radians(a) { return a*Math.PI/180; }
+ var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
+ var y = radians(b.lat-a.lat);
+ return Math.sqrt(x*x + y*y) * 6371000;
+}
+
+function draw() {
+ g.setColor(1, 1, 1);
+ g.fillRect(0, 25, W, H);
+ g.setFont('Vector', 60);
+
+ g.setColor(0, 0, 0);
+ g.setFontAlign(-1, 1);
+ let now = new Date();
+ g.drawString(now.getHours() + ":" + add0(now.getMinutes()), 10, 90);
+
+ every(now);
+
+ let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps;
+
+ g.setFont('Vector', 26);
+
+ const weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
+
+ g.drawString(weekday[now.getDay()] + "" + now.getDate() + ". " + km.toFixed(1) + "km", 10, 115);
+
+ if (gps_on) {
+ if (!last_restart) {
+ d = (getTime()-last_pause);
+ if (last_fix)
+ msg = "PL"+ (getTime()-last_fix).toFixed(0);
+ else
+ msg = "PN"+ (getTime()-gps_on).toFixed(0);
+
+ print("gps on, paused ", d, gps_needed);
+ if (d > gps_needed * 2) {
+ gpsRestart();
+ }
+ } else {
+ fix = Bangle.getGPSFix();
+ if (fix.fix && fix.lat) {
+ if (!prev_fix) {
+ prev_fix = fix;
+ }
+ msg = fix.speed.toFixed(1) + " km/h";
+ if (!last_fstart)
+ last_fstart = getTime();
+ last_fix = getTime();
+ gps_needed = 60;
+ loggps(fix);
+ print("GPS FIX", msg);
+ d = calcDistance(fix, prev_fix);
+ if (d > 30) {
+ prev_fix = fix;
+ gps_dist += d/1000;
+ }
+ } else {
+ if (last_fix)
+ msg = "L"+ (getTime()-last_fix).toFixed(0);
+ else
+ msg = "N"+ (getTime()-gps_on).toFixed(0);
+ }
+
+ d = (getTime()-last_restart);
+ d2 = (getTime()-last_fstart);
+ print("gps on, restarted ", d, gps_needed, d2, fix.lat);
+ if (d > gps_needed || (last_fstart && d2 > 10)) {
+ gpsPause();
+ gps_needed = gps_needed * 1.5;
+ print("Pausing, next try", gps_needed);
+ }
+ }
+ msg += " "+gps_dist.toFixed(1)+"km";
+ } else {
+ msg = note;
+ }
+ g.drawString(msg, 10, 145);
+ if (is_active) {
+ g.drawString("act " + (cur_altitude - alt_adjust).toFixed(0), 10, 175);
+ } else {
+ alt_adjust = cur_altitude - rest_altitude;
+ g.drawString(alt_adjust.toFixed(0) + "m " + cur_temperature.toFixed(1)+"C", 10, 175);
+ }
+ queueDraw();
+}
+
+function draw_all() {
+ g.setColor(0, 0, 0);
+ g.fillRect(0, 0, W, H);
+ g.setFont('Vector', 36);
+
+ g.setColor(1, 1, 1);
+ g.setFontAlign(-1, 1);
+ let now = new Date();
+ g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40);
+
+ acc = Bangle.getAccel();
+ let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y;
+ let diff = ax * ax + ay * ay + az * az;
+ diff = diff * 3;
+ if (diff > 1)
+ diff = 1;
+
+ co = Bangle.getCompass();
+ step = Bangle.getStepCount();
+ bat = E.getBattery();
+ Bangle.getPressure().then((x) => { alt = x.altitude; temp = x.temperature; },
+ print);
+
+ g.setColor(0, 1, 0);
+ g.drawCircle(cx, cy, sc);
+
+ if (0) {
+ g.setColor(0, 0.25, 0);
+ g.fillCircle(cx + sc * acc.x, cy + sc * acc.y, 5);
+ g.setColor(0, 0, 0.25);
+ g.fillCircle(cx + sc * acc.x, cy + sc * acc.z, 5);
+ }
+ if (0) {
+ print(co.dx, co.dy, co.dz);
+ g.setColor(0, 0.25, 0);
+ g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dy / 1500, 5);
+ g.setColor(0, 0, 0.25);
+ g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dz / 400, 5);
+ }
+ if (1) {
+ h = co.heading / 360 * 2 * Math.PI;
+ g.setColor(0, 0, 0.5);
+ g.fillCircle(cx + sc * Math.sin(h), cy + sc * Math.cos(h), 5);
+ }
+
+ g.setColor(1, 1, 1);
+
+ g.setFont('Vector', 22);
+ g.drawString(now.getDate()+"."+(now.getMonth()+1)+" "+now.getDay(), 3, 60);
+ g.drawString(msg, 3, 80);
+ g.drawString("S" + step + " B" + Math.round(bat/10) + (Bangle.isCharging()?"c":""), 3, 100);
+ g.drawString("A" + Math.round(alt) + " T" + Math.round(temp), 3, 120);
+ g.drawString("C" + Math.round(co.heading) + " B" + bpm, 3, 140);
+
+ queueDraw();
+}
+
+function accelTask() {
+ tm = 100;
+ acc = Bangle.getAccel();
+ en = !Bangle.isLocked();
+ if (en && acc.z < -0.95) {
+ msg = "Level";
+ buzz = ".-..";
+ tm = 3000;
+ }
+ if (en && acc.x < -0.80) {
+ msg = "Down";
+ buzz = "-..";
+ tm = 3000;
+ }
+ if (en && acc.x > 0.95) {
+ msg = "Up";
+ buzz = "..-";
+ tm = 3000;
+ }
+
+ setTimeout(accelTask, tm);
+}
+
+function buzzTask() {
+ if (buzz != "") {
+ now = buzz[0];
+ buzz = buzz.substring(1);
+ dot = 100;
+ if (now == " ") {
+ setTimeout(buzzTask, 300);
+ } else if (now == ".") {
+ Bangle.buzz(dot, 1);
+ setTimeout(buzzTask, 2*dot);
+ } else if (now == "-") {
+ Bangle.buzz(3*dot, 1);
+ setTimeout(buzzTask, 4*dot);
+ } else if (now == "/") {
+ setTimeout(buzzTask, 6*dot);
+ } else print("Unknown character -- ", now, buzz);
+ } else
+ setTimeout(buzzTask, 60000);
+}
+
+function aliveTask() {
+ function cmp(s) {
+ let d = acc[s] - last_acc[s];
+ return d < -0.03 || d > 0.03;
+ }
+ // HRM seems to detect hand quite nicely
+ acc = Bangle.getAccel();
+ is_active = false;
+ if (cmp("x") || cmp("y") || cmp("z")) {
+ print("active");
+ is_active = true;
+ }
+ last_acc = acc;
+
+ setTimeout(aliveTask, 60000);
+}
+
+var drawTimeout;
+
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ if (0) // FIXME
+ next = 60000;
+ else
+ next = 1000;
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, next - (Date.now() % next));
+
+}
+
+function start() {
+ Bangle.on("drag", touchHandler);
+ if (0)
+ Bangle.on("accel", accelHandler);
+ if (0) {
+ Bangle.setCompassPower(1, "sixths");
+ Bangle.setBarometerPower(1, "sixths");
+ Bangle.setHRMPower(1, "sixths");
+ Bangle.setGPSPower(1, "sixths");
+ Bangle.on("HRM", (hrm) => { bpm = hrm.bpm; } );
+ }
+
+ draw();
+ buzzTask();
+ //accelTask();
+
+ if (1) {
+ last_acc = Bangle.getAccel();
+ aliveTask();
+ }
+}
+
+g.reset();
+Bangle.setUI();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+let logfile = require("Storage").open("sixths.egt", "a");
+
+start();
diff --git a/apps/sixths/app.png b/apps/sixths/app.png
new file mode 100644
index 000000000..cf1e13d65
Binary files /dev/null and b/apps/sixths/app.png differ
diff --git a/apps/sixths/metadata.json b/apps/sixths/metadata.json
new file mode 100644
index 000000000..ece88348d
--- /dev/null
+++ b/apps/sixths/metadata.json
@@ -0,0 +1,13 @@
+{ "id": "sixths",
+ "name": "Sixth sense",
+ "version":"0.01",
+ "description": "Clock for outdoor use with GPS support",
+ "icon": "app.png",
+ "readme": "README.md",
+ "supports" : ["BANGLEJS2"],
+ "tags": "",
+ "storage": [
+ {"name":"sixths.app.js","url":"app.js"},
+ {"name":"sixths.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/snepwatch/ChangeLog b/apps/snepwatch/ChangeLog
new file mode 100644
index 000000000..fa064b2c0
--- /dev/null
+++ b/apps/snepwatch/ChangeLog
@@ -0,0 +1 @@
+1.00: Initial release
diff --git a/apps/snepwatch/README.md b/apps/snepwatch/README.md
new file mode 100644
index 000000000..7bf5ec5cd
--- /dev/null
+++ b/apps/snepwatch/README.md
@@ -0,0 +1,17 @@
+# Snepwatch
+
+
+
+Features:
+ * This watch face uses the Terminus font.
+ * Background, digit-outline, and digit-fill colours are all customizable from the settings menu
+ * Text can be configured to either black or white
+ * Today's step count is always shown at the bottom
+ * Heart-rate is shown when we have a valid reading within the last ten seconds
+ * The heart rate monitor can be activated by another app running in the background (eg, Recorder)
+ * Or, the heart rate monitor is activated when you unlock the screen (eg, by pressing the button)
+ * Sometimes a single unlock-period is not long enough to achieve a usable reading, so a second press can be used
+ * The heart rate monitor will be returned to deactivated when the screen remains locked for 15 seconds.
+ * Hidden widget-bar. Swipe down to see the widgets.
+ * "Sneptember" instead of "September", because snow leopards are excellent :3
+ * Fast Loading
diff --git a/apps/snepwatch/app.js b/apps/snepwatch/app.js
new file mode 100644
index 000000000..833285644
--- /dev/null
+++ b/apps/snepwatch/app.js
@@ -0,0 +1,242 @@
+/*
+ * SnepWatch - Bangle JS 2 Port
+ * JoppyFurr 2023
+ */
+
+{
+ Graphics.prototype.setFontTerminus_14 = function (scale) {
+ g.setFontCustom (atob ("AB/oJhGIZBf4AAAAAgUB/8AQBAAAGDgWCYRiFwQAABhoBiGIYhd4AAABgKBIIhCP/AAAPiiGIYhiGHgAAA/khiGIYhB4AAAgCAIHhiYOAAAAHeiGIYhiF3gAAB4IRhGEYSfwAEAoOjEyMXBQCAA="), 48, 8, 10);
+ };
+
+ Graphics.prototype.setFontTerminus_18 = function (scale) {
+ /* TODO: Strip out unused characters - Eg, encode zero-width */
+ g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("AH4A/AH4AnoEHBpcgB4MAwEBgECgEEgEIItf/4UEokQxFBiMCkkEn/wCY0CgMIgU//hHCiAQGh0GkEUoFIwMRgkiiFD4GACQkwmGAiMIBwNEkA7Bh98EoqPBiUAog6BgkQiEgv/4CIvjhFBiGCkMEoUIwkRg/ACQv8IoIODHYVAj43FuC4EgfEjEI4EeOA334RZEHYc9SosD8EIkMQoUgwlBhGCkH/QQoA/AH6VD/+IkDHBkGAoMAwUA//gEq8/+GAiMAkUAokAxEBhkMCIkB//iBwkQgUQhEfG7Ef/0hgFCgGEgMIgUAgip/AAM4gEIgEQQwVH/4HBAFEH/+EgEGcwIDCmEAwDtB4CVERgIBGDoOAcB8P/jgHhEEbYMgiFAkGAnylrnkwwkRhEiiFEkGIoMMvhKFgD+Dh//XYMggFAJVd//AxBgOAhkAjEAAYMEn6XFAH4A/AE8PgMhgVCgmEhMIiUQj/gCIkf/0EgkIhEQiEgkFAoF/EgsH+AOFwGBgMBggSFj/ABwkCgQsBfwQAEv8BiMCkUEokIxERiEeoBvXJQUSkEkoFIwGRgMj/+ACIcB//gFoOAgMAgUALQMHJQwACLIM3/kAhC6bO4I0DhUAkUAwQsBCpBEBx//CwQAagbgBLAUQgEggB3Bv6CEcBh1BAAk//jgIDoJKZQQJMBJQpJFACodCh/+JQbRHUQLwFQ4U/8BKFH4PAgFggEwgOAhkACIoAmJQUggVAgmAhMAiU//AiVO4MEiEEoEFgFEoFBAYMAqCOCgE+gC6BAIkH/Ef8E/wAOBAAI="))), 32, 10, 15);
+ };
+
+ Graphics.prototype.setFontDigits = function (scale) {
+ g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("AEE//4AC4EDAof8Bid8EQMB+ED8/AAwMfgE+BisD8AMEnwIBBisHBAQMCCoYMUh+ABgn4AoIMUTbIMGAH4AhdoKOC8DtBAwTYBBiQHBBgbtCBi1/VAf/84FE/wMSFYZNDAAY4EBmoA7n7PCg/+gf/BQV/8AMSvgLCh+D8/AAwX4nwMWj6OBBgUB+EDBiysBBgasCBiwIDAYMHCoQMTRwP4dIc//+ABiz/8AAbpD//AewcP/gMTdIaOEAAT2EBiLCdBn7pDeQKOC/+D/6bDBiIA/YwbyCAATyDBikH4IMEvkfBi0PwAME/EBBiwDBBgYDCBi0+BAZrB8AMXv7tBAAXnAon+BiQrBAH0HIYP8AgPAJoXAbAIMTvgDCSYPnCASTCBn4M2gE/RQIQDS4QMSf/gAEgJLBeQcfAoRNBBiUD8BwD8E+RwgMSdP4MhUgTpFewYMVAHprCAAZrCBnf//wMDh//WoQMTn/waYn/GYgMRh7GDBgLTDBid/FQQMC/4qBBioA/QoX/wf/OAKSB/AIBOAIMSRwKxDZ4SxCnwM/BmibZBgwA/JoREDJoJRBBiqFDRwkD8CbDBn4MWeQIMEvj6DBh6OCAAOARwQAB/CbDBiAA/AH4ACh/8gEH/wGB/6xBv7YBBiYA/AH4A=="))), 48, 40, 52);
+ };
+
+ Graphics.prototype.setFontOutline = function (scale) {
+ g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("ADMD/4AC/kAoALDkEEAocCBhkH4AGDn2DEgP+h/8jEgC4WAoOCC4UIggMJnwMEgkfgAMDoIIBBgcQBhV8BgkInwMECoYMCEQQMI/AMEiF8BgcBOwQMDQYQMHn/wv59B4IdCVoXgVDEAn7JDVogA/ACEB+AECXwLtBRwUQfQIMCbAIMLSwIMEoImDoL6CBgg4DBg0X4KbD4IkCAAUYKQoMSi4lDEwILEHAhnDBmKoBAGUD/4ECv/goDPChEBgkASAWAiAMLi/ABgX4gODEwUQvkYkCzCglBwQyCiFEBhMB+AMEkH4BgcChEEBgmABhMD8AMEoPwBgasCBggIBBhEHfQIMDwfgBgcICoQMCEQYMGn//Bgd8EwPAF4VAgABBBgKbBAIIMPEwKvCnz/vAAcDbgUP/hLBdIMBW4L2DiECBhh4BBgc+wYmEbgabEAATpEBgibDWpoM/BhrcBPoP/4N8M4cD8D0BogZBTYUEBAKoCBhYmEEQIA1n/waYjyCAAT/CBh59BBgcInwMEwUgBgkiBhX4BgkQvgMDgMEgQMEoAMJgPwBgkg/AMDAYQMEwAMJa4IMEoKFBBgUEiAVDBgIiDBg0H4LaB8H//yQEgEYAokBBiUXEoIAC/z/ygF/G4PAcoKQDbAKdEbAIMSn2D//4BgN8jAQCUAQDBDoKgBBn4MqPoP/4IMBg6oBgfoCASXCDoVAS4IdDBhAdDZYYAzj44B//8c4QACKQIBBeQYMMnwMDn0Ej/4n4mBiFBTYkEiB9DBg18BgkYEwLp/BjWDUQPBvkAcofgCwL2DSQT2CAoQMMgYmBEQIAyLQIADNYQADPAQM0h//9AMCgIBBBgcQgC1CBhibBBglAEwi1BwAzDoEIcAgMEXIIMDiAmCBgI3BHgQMCaYYMGn/wBgYoBcAYoCOgYMBAIIMOg//+D/zAAZ4B/E///8SQMAogLBkCSBggIBOAIMLg54BvgMBn2DEwjPCXwVBAYK+DBn4MoPAJ9B//BQoKKCgfgVDEAEwgiBAGUDG4pEDAAJNBKIIMOPAkAnyFDTYkEiCbDAYIM/Bh18BgkInwMEwT6DBgMgBgzcB/kf/EgvitDAgKbBTIabCUwYMMEwIAB/D/wAH4AG//AgF/8EAiECgEIgMAAIK+BwDYBBiQmGAH4A5"))), 48, 40, 52);
+ };
+
+ var snepwatch_tick_timeout;
+ var snepwatch_hrm_timeout;
+ var snepwatch_hrm_show_timeout;
+ var heart_rate = 0;
+ var heart_rate_time = 0;
+
+ /* Load settings */
+ var settings = Object.assign ({
+ /* Default Values */
+ outline_r: 1,
+ outline_g: 0,
+ outline_b: 0,
+ fill_r: 0.5,
+ fill_g: 0,
+ fill_b: 0,
+ bg_r: 0,
+ bg_g: 0,
+ bg_b: 0,
+ text: 1,
+ }, require ('Storage').readJSON ("snepwatch.json", true) || {});
+
+ /*
+ * Tick once per minute.
+ */
+ let snepwatch_tick_queue = function () {
+ if (snepwatch_tick_timeout) {
+ clearTimeout (snepwatch_tick_timeout);
+ }
+
+ snepwatch_tick_timeout = setTimeout (function () {
+ snepwatch_tick_timeout = undefined;
+ snepwatch_tick ();
+ }, 60000 - (Date.now () % 60000));
+ };
+
+
+ /*
+ * Draw the heart rate sensor reading.
+ * The reading is only shown if it is from within the last 10 seconds.
+ * Assumes the Terminus_18 font is already selected.
+ */
+ let draw_heart_rate = function () {
+ let heart_rate_string = "--";
+ let hrm_show = false;
+
+ /* As we are about to show the heart rate,
+ * previously set timers are considered invalid */
+ if (snepwatch_hrm_show_timeout) {
+ clearTimeout (snepwatch_hrm_show_timeout);
+ }
+
+ /* Only show the heart rate if the measurement is recent */
+ if (heart_rate_time > Date.now () - 10000) {
+ hrm_show = true;
+ heart_rate_string = "" + heart_rate;
+ }
+
+ g.clearRect (17, 160, 88, 175);
+ g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text);
+ g.drawString (heart_rate_string, 17, 160);
+
+ /* If the heart rate was shown, check back when the reading
+ * would become stale so that it can be cleared. */
+ if (hrm_show) {
+ snepwatch_hrm_show_timeout = setTimeout (function () {
+ snepwatch_hrm_show_timeout = undefined;
+ draw_heart_rate ();
+ }, heart_rate_time + 10000 - Date.now ());
+ }
+ };
+
+
+ /*
+ * Called once per minute.
+ *
+ * Updates the time, date, and battery level.
+ */
+ let snepwatch_tick = function () {
+ /* Data */
+ let days = [ "Sun ", "Mon ", "Tue ", "Wed ", "Thu ", "Fri ", "Sat " ];
+ let months = [ " Jan", " Feb", " Mar", " Apr", " May", " June", " July", " Aug", " Snep", " Oct", " Nov", " Dec"];
+ let date = new Date ();
+ let charge_level = E.getBattery ();
+
+ /* Clear */
+ g.reset ();
+ g.setBgColor (settings.bg_r, settings.bg_g, settings.bg_b);
+ g.clear ();
+
+ /* Battery level - Note, '%' is encoded as ':' */
+ let battery_text = ((charge_level < 10) ? "0" : "") + charge_level + ":";
+ if (charge_level <= 16) {
+ g.setColor (1, 0, 0);
+ } else {
+ g.setColor (0, 0 + settings.text, 1);
+ }
+ g.setFont ("Terminus_14");
+ g.drawString (battery_text, 2, 2);
+
+ /* Date */
+ let day = days [ date.getDay () ];
+ let dd = date.getDate ();
+ dd = ((dd < 10) ? "0" : "") + dd;
+ let month = months [ date.getMonth () ];
+
+ let date_text = day + dd + month;
+ if (date_text.length < 11) {
+ date_text = " " + date_text;
+ }
+ g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text);
+ g.setFont ("Terminus_18");
+ g.drawString (date_text, 65, 2);
+
+ /* Time */
+ let hours = date.getHours ();
+ let minutes = date.getMinutes ();
+ let time_hh = ((hours < 10) ? "0" : "") + hours;
+ let time_mm = ((minutes < 10) ? "0" : "") + minutes;
+ g.setColor (settings.fill_r, settings.fill_g, settings.fill_b);
+ g.setFont ("Digits");
+ g.drawString (time_hh, -2, 60);
+ g.drawString (":", 71, 55);
+ g.drawString (time_mm, 98, 60);
+
+ g.setColor (settings.outline_r, settings.outline_g, settings.outline_b);
+ g.setFont ("Outline");
+ g.drawString (time_hh, -2, 60);
+ g.drawString (":", 71, 55);
+ g.drawString (time_mm, 98, 60);
+
+
+ /* Steps so far for the day */
+ let steps = Bangle.getHealthStatus ('day').steps;
+ let steps_string = "" + steps;
+ if (steps >= 1000) {
+ steps_string = steps_string.slice (0, -3) + "," + steps_string.slice (-3);
+ }
+
+ g.setFont("Terminus_18");
+ /* With dark text, use blue for the step symbol.
+ With light text, use green for the step symbol. */
+ g.setColor (0, 0 + settings.text, 1 - settings.text);
+ g.drawString ("{", 2, 144); /* Arrows */
+ g.setColor (1, 0, 0);
+ g.drawString ("|", 2, 160); /* Heart */
+ g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text);
+ g.drawString (steps_string, 17, 144);
+ draw_heart_rate ();
+
+ /* Queue up the next tick */
+ snepwatch_tick_queue ();
+ };
+
+
+ /* Callback for when the backlight state changes */
+ let display_cb = lock => {
+ if (lock) {
+ /* The backlight may not run for long enough to get a good reading.
+ Wait 15 seconds with the backlight off before disabling the sensor. */
+ snepwatch_hrm_timeout = setTimeout (function () {
+ snepwatch_hrm_timeout = undefined;
+ Bangle.setHRMPower (false, "snepwatch");
+ }, 15000);
+ } else {
+ if (snepwatch_hrm_timeout) {
+ clearTimeout (snepwatch_hrm_timeout);
+ snepwatch_hrm_timeout = undefined;
+ }
+ Bangle.setHRMPower (true, "snepwatch");
+ }
+ };
+
+ /* Callback for the heart rate monitor */
+ let heart_rate_cb = hrm => {
+ if (hrm.bpm > 0 && hrm.confidence > 50) {
+ heart_rate = hrm.bpm;
+ heart_rate_time = Date.now ();
+ }
+
+ g.setFont("Terminus_18");
+ draw_heart_rate ();
+ };
+
+ let previous_theme = g.theme;
+ g.setTheme ( { bg:"#000", fg:"#fff", dark:true } );
+
+ /* Initial call, will tick once per minute */
+ snepwatch_tick ();
+ Bangle.on ('lock', display_cb);
+ Bangle.on ('HRM', heart_rate_cb);
+
+ /* Use a swipe to show the widgets */
+ Bangle.loadWidgets ();
+ require ("widget_utils").swipeOn ();
+
+ /* Allow for Fast Loading */
+ Bangle.setUI ( { mode:"clock", remove:function () {
+ if (snepwatch_tick_timeout) {
+ if (snepwatch_tick_timeout) {
+ clearTimeout (snepwatch_tick_timeout);
+ }
+ if (snepwatch_hrm_timeout) {
+ clearTimeout (snepwatch_hrm_timeout);
+ }
+ if (snepwatch_hrm_show_timeout) {
+ clearTimeout (snepwatch_hrm_show_timeout);
+ }
+ Bangle.removeListener('lcdPower', display_cb);
+ Bangle.removeListener('HRM', heart_rate_cb);
+ Bangle.setHRMPower (false, "snepwatch");
+ delete Graphics.prototype.setFontTerminus_14;
+ delete Graphics.prototype.setFontTerminus_18;
+ delete Graphics.prototype.setFontDigits;
+ delete Graphics.prototype.setFontOutline;
+ g.setTheme (previous_theme);
+ require ("widget_utils").show();
+ }
+ } } );
+}
diff --git a/apps/snepwatch/app.png b/apps/snepwatch/app.png
new file mode 100644
index 000000000..50eb5c388
Binary files /dev/null and b/apps/snepwatch/app.png differ
diff --git a/apps/snepwatch/metadata.json b/apps/snepwatch/metadata.json
new file mode 100644
index 000000000..b33f22d11
--- /dev/null
+++ b/apps/snepwatch/metadata.json
@@ -0,0 +1,19 @@
+{
+ "id": "snepwatch",
+ "name": "Snepwatch",
+ "version": "1.00",
+ "description": "A configurable watch face using the Terminus font",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"snepwatch.app.js","url":"app.js"},
+ {"name":"snepwatch.settings.js","url":"settings.js"},
+ {"name":"snepwatch.img","url":"snepwatch.img","evaluate":false}
+ ],
+ "data":[{"name":"snepwatch.settings.json"}]
+}
diff --git a/apps/snepwatch/screenshot-dark.png b/apps/snepwatch/screenshot-dark.png
new file mode 100644
index 000000000..d9a1ccb51
Binary files /dev/null and b/apps/snepwatch/screenshot-dark.png differ
diff --git a/apps/snepwatch/screenshot-light.png b/apps/snepwatch/screenshot-light.png
new file mode 100644
index 000000000..074747423
Binary files /dev/null and b/apps/snepwatch/screenshot-light.png differ
diff --git a/apps/snepwatch/settings.js b/apps/snepwatch/settings.js
new file mode 100644
index 000000000..57ae94d52
--- /dev/null
+++ b/apps/snepwatch/settings.js
@@ -0,0 +1,81 @@
+(
+ function(back) {
+ var FILE = "snepwatch.json";
+
+ /* Load settings */
+ var settings = Object.assign ({
+ /* Default Values */
+ outline_r: 1,
+ outline_g: 0,
+ outline_b: 0,
+ fill_r: 0.5,
+ fill_g: 0,
+ fill_b: 0,
+ bg_r: 0,
+ bg_g: 0,
+ bg_b: 0,
+ text: 1
+ ,
+ }, require ('Storage').readJSON (FILE, true) || {});
+
+ function write_settings () {
+ require ('Storage').writeJSON (FILE, settings);
+ }
+
+ /* Show the menu */
+ var main_menu = {
+ "" : { "title": "Snepwatch",
+ back : function() { back (); }},
+ "Outline Colour": function () { E.showMenu (outline_menu); },
+ "Fill Colour": function () { E.showMenu (fill_menu); },
+ "Background Colour": function () { E.showMenu (background_menu); },
+ "Text": { value: (settings.text == 1),
+ format: v => v ? "Light" : "Dark",
+ onchange: v => { settings.text = v; write_settings (); }},
+ };
+
+ var outline_menu = {
+ "": { title : "Outline Colour",
+ back : function() { E.showMenu (main_menu); } },
+ "Red": { value: settings.outline_r,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.outline_r = v; write_settings (); }},
+ "Green": { value: settings.outline_g,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.outline_g = v; write_settings (); }},
+ "Blue": { value: settings.outline_b,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.outline_b = v; write_settings (); }},
+ };
+
+ var fill_menu = {
+ "" : { title : "Fill Colour",
+ back : function() { E.showMenu (main_menu); } },
+ "Red": { value: settings.fill_r,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.fill_r = v; write_settings (); }},
+ "Green": { value: settings.fill_g,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.fill_g = v; write_settings (); }},
+ "Blue": { value: settings.fill_b,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.fill_b = v; write_settings (); }},
+ };
+
+ var background_menu = {
+ "" : { title : "Background Colour",
+ back : function() { E.showMenu (main_menu); } },
+ "Red": { value: settings.bg_r,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.bg_r = v; write_settings (); }},
+ "Green": { value: settings.bg_g,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.bg_g = v; write_settings (); }},
+ "Blue": { value: settings.bg_b,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.bg_b = v; write_settings (); }},
+ };
+
+ E.showMenu (main_menu);
+ }
+)
diff --git a/apps/snepwatch/snepwatch.img b/apps/snepwatch/snepwatch.img
new file mode 100644
index 000000000..ae0527cbb
Binary files /dev/null and b/apps/snepwatch/snepwatch.img differ
diff --git a/apps/stacker/ChangeLog b/apps/stacker/ChangeLog
new file mode 100644
index 000000000..b0d975391
--- /dev/null
+++ b/apps/stacker/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Optimizations
\ No newline at end of file
diff --git a/apps/stacker/README.md b/apps/stacker/README.md
new file mode 100644
index 000000000..5be5b7bee
--- /dev/null
+++ b/apps/stacker/README.md
@@ -0,0 +1,12 @@
+# Stacker
+
+A simple game of stacking cubes.
+
+
+## Usage
+
+Press the button to stack!
+
+## Creator
+
+NovaDawn999
diff --git a/apps/stacker/app-icon.js b/apps/stacker/app-icon.js
new file mode 100644
index 000000000..37130b23f
--- /dev/null
+++ b/apps/stacker/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgIcZh////AAoMIAQNAAq8D//IDrQFFn//DrcH/+IDqx9DAQIADwEAggOBkAF/AuJ9FA=="))
\ No newline at end of file
diff --git a/apps/stacker/app.js b/apps/stacker/app.js
new file mode 100644
index 000000000..a486e06cc
--- /dev/null
+++ b/apps/stacker/app.js
@@ -0,0 +1,135 @@
+const HARDWARE_VERSION = process.env.HWVERSION;
+const BUTTON = HARDWARE_VERSION === 2 ? BTN : BTN2;
+const TICKRATE = 69;
+const BLOCK_SIZE = 16;
+const GAMEBOARD_X = 16;
+const GAMEBOARD_WIDTH = g.getWidth() - 16 - BLOCK_SIZE;
+const START_Y = g.getHeight() - BLOCK_SIZE - 1;
+const START_LENGTH = 4;
+var length;
+var updateTimeout;
+var rows = [];
+var gameState = ""; //win, lose, play
+
+function Block (x, y, match) {
+ this.x = x;
+ this.y = y;
+ this.match = match;
+ this.show = true;
+}
+
+class Row {
+ constructor(x, y, size, direction, match) {
+ this.y = y;
+ this.size = size;
+ this.blocks = [];
+ if (Math.random() > 0.49) {
+ this.direction = 1;
+ this.x = BLOCK_SIZE;
+ }
+ else {
+ this.direction = -1;
+ this.x = g.getWidth() - this.size * BLOCK_SIZE;
+ }
+ this.match = match;
+ for (var i = 0; i < size; i++) {
+ var b = new Block(this.x + (BLOCK_SIZE * i), this.y, this.match);
+ this.blocks.push(b);
+ }
+ }
+ update() {
+ this.x += BLOCK_SIZE * this.direction;
+ if (this.x + (this.size * BLOCK_SIZE) > GAMEBOARD_X + GAMEBOARD_WIDTH || this.x < GAMEBOARD_X) {
+ this.direction = -this.direction;
+ }
+ for (var i = 0; i < this.size; i++) {
+ this.blocks[i].x = this.x + BLOCK_SIZE * i;
+ }
+ }
+ draw() {
+ for (var i = 0; i < this.size; i++) {
+ if (this.blocks[i].show) {
+ g.drawRect({x: this.blocks[i].x, y: this.y, w: BLOCK_SIZE, h: BLOCK_SIZE});
+ }
+ }
+ }
+}
+
+
+
+function init() {
+ Bangle.setLCDPower(1);
+ g.setTheme({bg:"#000", fg:"#fff", dark:true}).clear();
+ setInterval(update, TICKRATE);
+ setWatch(handleInput, BUTTON, {repeat:true});
+ changeState("play");
+}
+
+function update() {
+ "ram"
+ if (gameState === "play") {
+ g.clear(reset);
+ rows[rows.length - 1].update();
+ rows.forEach(row => row.draw());
+ g.flip();
+ }
+}
+
+function changeState(gs) {
+ gameState = gs;
+ g.clear(reset);
+ switch(gameState) {
+ case "win":
+ E.showMessage("YOU WIN!");
+ break;
+ case "lose":
+ E.showMessage("YOU LOSE!");
+ break;
+ case "play":
+ rows = [];
+ length = START_LENGTH;
+ var first = new Row(GAMEBOARD_X, START_Y, length, 1, true);
+ rows.push(first);
+ break;
+ }
+}
+
+function collapse() {
+ "ram"
+ for (var i = 0; i < rows[rows.length - 1].blocks.length; i++) {
+ for (var j = 0; j < rows[rows.length -2].blocks.length; j++) {
+ if (rows[rows.length - 1].blocks[i].x === rows[rows.length - 2].blocks[j].x) {
+ if (rows[rows.length - 2].blocks[j].match === true)
+ rows[rows.length - 1].blocks[i].match = true;
+ }
+ }
+ }
+ for (var y = 0; y < rows[rows.length - 1].blocks.length; y++) {
+ if (rows[rows.length - 1].blocks[y].match === false) {
+ length -= 1;
+ if (length < 1) {
+ changeState("lose");
+ }
+ rows[rows.length - 1].blocks[y].show = false;
+ }
+ }
+}
+
+function handleInput() {
+ if (gameState === "win" || gameState === "lose") {
+ changeState("play");
+ }
+ else {
+ if (rows.length > 1) {
+ collapse();
+ if (rows[rows.length - 1].y <= -1) {
+ changeState("win");
+ }
+ }
+ var r = new Row(GAMEBOARD_X + Math.round(length/2) * BLOCK_SIZE, rows[rows.length - 1].y - BLOCK_SIZE, length, 1, false);
+ rows.push(r);
+ }
+}
+
+init();
+update();
\ No newline at end of file
diff --git a/apps/stacker/app.png b/apps/stacker/app.png
new file mode 100644
index 000000000..35683688a
Binary files /dev/null and b/apps/stacker/app.png differ
diff --git a/apps/stacker/metadata.json b/apps/stacker/metadata.json
new file mode 100644
index 000000000..abaf49a6d
--- /dev/null
+++ b/apps/stacker/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "stacker",
+ "name": "Stacker",
+ "shortName":"Stacker",
+ "version":"0.02",
+ "description": "Game of Stacking",
+ "icon": "app.png",
+ "tags": "game",
+ "supports" : ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"stacker.app.js","url":"app.js"},
+ {"name":"stacker.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog
index c4f382aa9..cb016df1a 100644
--- a/apps/stopwatch/ChangeLog
+++ b/apps/stopwatch/ChangeLog
@@ -2,3 +2,4 @@
0.02: Adjust for touch events outside of screen g dimensions
0.03: Do not register as watch, manually start clock on button
0.04: Keep running in background by saving state
+0.05: Fast Loading support
diff --git a/apps/stopwatch/metadata.json b/apps/stopwatch/metadata.json
index bbc2dc181..27cdacb71 100644
--- a/apps/stopwatch/metadata.json
+++ b/apps/stopwatch/metadata.json
@@ -1,7 +1,7 @@
{
"id": "stopwatch",
"name": "Stopwatch Touch",
- "version": "0.04",
+ "version": "0.05",
"description": "A touch based stop watch for Bangle JS 2",
"icon": "stopwatch.png",
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js
index d98f06cdd..0d3ec364e 100644
--- a/apps/stopwatch/stopwatch.app.js
+++ b/apps/stopwatch/stopwatch.app.js
@@ -1,3 +1,4 @@
+{
const CONFIGFILE = "stopwatch.json";
const now = Date.now();
@@ -20,6 +21,7 @@ let timeY = 2*h/5;
let displayInterval;
let redrawButtons = true;
const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size
+const origTheme = g.theme;
// 24 pixel images, scale to watch
// 1 bit optimal, image string, no E.toArrayBuffer()
@@ -27,19 +29,19 @@ const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wY
const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA=");
const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w==");
-function saveState() {
+const saveState = function() {
config.state.total = tTotal;
config.state.start = tStart;
config.state.current = tCurrent;
config.state.running = running;
require("Storage").writeJSON(CONFIGFILE, config);
-}
+};
-function log_debug(o) {
+const log_debug = function(o) {
//console.log(o);
-}
+};
-function timeToText(t) {
+const timeToText = function(t) {
let hrs = Math.floor(t/3600000);
let mins = Math.floor(t/60000)%60;
let secs = Math.floor(t/1000)%60;
@@ -53,9 +55,9 @@ function timeToText(t) {
//log_debug(text);
return text;
-}
+};
-function drawButtons() {
+const drawButtons = function() {
log_debug("drawButtons()");
if (!running && tCurrent == tTotal) {
bigPlayPauseBtn.draw();
@@ -65,11 +67,11 @@ function drawButtons() {
} else {
bigPlayPauseBtn.draw();
}
-
- redrawButtons = false;
-}
-function drawTime() {
+ redrawButtons = false;
+};
+
+const drawTime = function() {
log_debug("drawTime()");
let Tt = tCurrent-tTotal;
let Ttxt = timeToText(Tt);
@@ -80,32 +82,32 @@ function drawTime() {
g.clearRect(0, timeY - 21, w, timeY + 21);
g.setColor(g.theme.fg);
g.drawString(Ttxt, w/2, timeY);
-}
+};
-function draw() {
+const draw = function() {
let last = tCurrent;
if (running) tCurrent = Date.now();
g.setColor(g.theme.fg);
if (redrawButtons) drawButtons();
drawTime();
-}
+};
-function startTimer() {
+const startTimer = function() {
log_debug("startTimer()");
draw();
displayInterval = setInterval(draw, 100);
-}
+};
-function stopTimer() {
+const stopTimer = function() {
log_debug("stopTimer()");
if (displayInterval) {
clearInterval(displayInterval);
displayInterval = undefined;
}
-}
+};
// BTN stop start
-function stopStart() {
+const stopStart = function() {
log_debug("stopStart()");
if (running)
@@ -127,9 +129,9 @@ function stopStart() {
draw();
}
saveState();
-}
+};
-function setButtonImages() {
+const setButtonImages = function() {
if (running) {
bigPlayPauseBtn.setImage(pause_img);
smallPlayPauseBtn.setImage(pause_img);
@@ -139,10 +141,10 @@ function setButtonImages() {
smallPlayPauseBtn.setImage(play_img);
resetBtn.setImage(reset_img);
}
-}
+};
// lap or reset
-function lapReset() {
+const lapReset = function() {
log_debug("lapReset()");
if (!running && tStart != tCurrent) {
redrawButtons = true;
@@ -152,10 +154,10 @@ function lapReset() {
draw();
}
saveState();
-}
+};
// simple on screen button class
-function BUTTON(name,x,y,w,h,c,f,i) {
+const BUTTON = function(name,x,y,w,h,c,f,i) {
this.name = name;
this.x = x;
this.y = y;
@@ -164,16 +166,16 @@ function BUTTON(name,x,y,w,h,c,f,i) {
this.color = c;
this.callback = f;
this.img = i;
-}
+};
BUTTON.prototype.setImage = function(i) {
this.img = i;
-}
+};
// if pressed the callback
BUTTON.prototype.check = function(x,y) {
//console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
-
+
if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
log_debug(this.name + ":callback\n");
this.callback();
@@ -197,48 +199,52 @@ BUTTON.prototype.draw = function() {
};
-var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
-var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
-var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
+const bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
+const smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
+const resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
bigPlayPauseBtn.setImage(play_img);
smallPlayPauseBtn.setImage(play_img);
resetBtn.setImage(pause_img);
+Bangle.setUI({mode:"custom", btn:() => load(), touch: (button,xy) => {
+ let x = xy.x;
+ let y = xy.y;
-Bangle.on('touch', function(button, xy) {
- var x = xy.x;
- var y = xy.y;
+ // adjust for outside the dimension of the screen
+ // http://forum.espruino.com/conversations/371867/#comment16406025
+ if (y > h) y = h;
+ if (y < 0) y = 0;
+ if (x > w) x = w;
+ if (x < 0) x = 0;
- // adjust for outside the dimension of the screen
- // http://forum.espruino.com/conversations/371867/#comment16406025
- if (y > h) y = h;
- if (y < 0) y = 0;
- if (x > w) x = w;
- if (x < 0) x = 0;
+ // not running, and reset
+ if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return;
- // not running, and reset
- if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return;
+ // paused and hit play
+ if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return;
- // paused and hit play
- if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return;
+ // paused and press reset
+ if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return;
- // paused and press reset
- if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return;
-
- // must be running
- if (running && bigPlayPauseBtn.check(x, y)) return;
-});
+ // must be running
+ if (running && bigPlayPauseBtn.check(x, y)) return;
+ }, remove: () => {
+ if (displayInterval) {
+ clearInterval(displayInterval);
+ displayInterval = undefined;
+ }
+ Bangle.removeListener('lcdPower',onLCDPower);
+ g.setTheme(origTheme);
+}});
// Stop updates when LCD is off, restart when on
-Bangle.on('lcdPower',on=>{
+const onLCDPower = (on) => {
if (on) {
draw(); // draw immediately, queue redraw
- } else { // stop draw timer
- if (drawTimeout) clearTimeout(drawTimeout);
- drawTimeout = undefined;
}
-});
+};
+Bangle.on('lcdPower',onLCDPower);
// Clear the screen once, at startup
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
@@ -254,4 +260,4 @@ if (running) {
} else {
draw();
}
-setWatch(() => load(), BTN, { repeat: false, edge: "falling" });
+}
diff --git a/apps/taglaunch/ChangeLog b/apps/taglaunch/ChangeLog
index 55315bf6e..6c36d39d5 100644
--- a/apps/taglaunch/ChangeLog
+++ b/apps/taglaunch/ChangeLog
@@ -2,3 +2,4 @@
0.02: Use Bangle.showClock for changing to clock (Backport from launch)
0.03: Remove app from 'tool' when it has at least one other known tag
Add tag 'health' for apps like Heart Rate Monitor
+0.04: Fix remove handler
diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js
index aad61e298..9569cc7bd 100644
--- a/apps/taglaunch/app.js
+++ b/apps/taglaunch/app.js
@@ -1,15 +1,14 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
let s = require("Storage");
-// TODO: Move icons to separate files
-// TODO: Allow change sortorder in settings
-let tags = {"clock": {name: /*LANG*/"Clocks", icon: atob("MDCEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERESIiIiIREREREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERESIiIiIgz//8ziIiIiIREREREREREREREiIiIg////////ziIiIhERERERERERERIiIiD//////////84iIiERERERERERESIiIg/////8A/////ziIiIRERERERERESIiI//////8A//////+IiIREREREREREiIiP//////8A///////4iIhERERERERIiIg///////8A///////ziIiERERERERIiIP///////8A////////OIiERERERESIiI////////8A////////+IiIRERERESIiD////////8A////////84iIRERERESIiP////////8A/////////4iIRERERESIiP////////8A/////////4iIREREREiIg/////////8A/////////ziIhEREREiIg/////////IAL////////ziIhEREREiIj////////yAAAv////////iIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIj////////yAAAD////////iIhEREREiIg/////////IAABP//////ziIhEREREiIg///////////IAE//////ziIhERERESIiP//////////8gAT/////4iIRERERESIiP///////////yABP////4iIRERERESIiD////////////IAL///84iIRERERESIiI////////////8i////+IiIRERERERIiIP/////////////////OIiERERERERIiIg////////////////ziIiEREREREREiIiP///////////////4iIhERERERERESIiI//////////////+IiIRERERERERESIiIg/////8i/////ziIiIRERERERERERIiIiD////8i////84iIiEREREREREREREiIiIg///8i///ziIiIhERERERERERERESIiIiIgz8i8ziIiIiIREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERERERESIiIiIRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA==")},
- "game": {name: /*LANG*/"Games", sortorder: 1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzIiIiIiIiIiIiIiIiIiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8RMxERABEAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IhERERERAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzL///IiIi////IiIi///xERAAAAAAAAAzL///IiIi////IiIi//8xERAAAAAAAAAzL///IiIi////IiIi//8hEREAAAAAAAAzL///IiIi////IiIi//8REREAAAAAAAAzL///IiIi////IiIi//MREREAAAAAAAAzIiIiIiIiIiIiIiIiIzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMhEREREREAAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "bluetooth": {name: /*LANG*/"Bluetooth", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "outdoors": {name: /*LANG*/"Outdoor", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "misc": {name: /*LANG*/"Misc", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "health": {name: /*LANG*/"Health"},
+// TODO: Allow to change sortorder in settings
+let tags = {"clock": {name: /*LANG*/"Clocks", icon: () => atob("MDCEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERESIiIiIREREREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERESIiIiIgz//8ziIiIiIREREREREREREREiIiIg////////ziIiIhERERERERERERIiIiD//////////84iIiERERERERERESIiIg/////8A/////ziIiIRERERERERESIiI//////8A//////+IiIREREREREREiIiP//////8A///////4iIhERERERERIiIg///////8A///////ziIiERERERERIiIP///////8A////////OIiERERERESIiI////////8A////////+IiIRERERESIiD////////8A////////84iIRERERESIiP////////8A/////////4iIRERERESIiP////////8A/////////4iIREREREiIg/////////8A/////////ziIhEREREiIg/////////IAL////////ziIhEREREiIj////////yAAAv////////iIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIj////////yAAAD////////iIhEREREiIg/////////IAABP//////ziIhEREREiIg///////////IAE//////ziIhERERESIiP//////////8gAT/////4iIRERERESIiP///////////yABP////4iIRERERESIiD////////////IAL///84iIRERERESIiI////////////8i////+IiIRERERERIiIP/////////////////OIiERERERERIiIg////////////////ziIiEREREREREiIiP///////////////4iIhERERERERESIiI//////////////+IiIRERERERERESIiIg/////8i/////ziIiIRERERERERERIiIiD////8i////84iIiEREREREREREREiIiIg///8i///ziIiIhERERERERERERESIiIiIgz8i8ziIiIiIREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERERERESIiIiIRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA==")},
+ "game": {name: /*LANG*/"Games", sortorder: 1, icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzIiIiIiIiIiIiIiIiIiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8RMxERABEAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IhERERERAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzL///IiIi////IiIi///xERAAAAAAAAAzL///IiIi////IiIi//8xERAAAAAAAAAzL///IiIi////IiIi//8hEREAAAAAAAAzL///IiIi////IiIi//8REREAAAAAAAAzL///IiIi////IiIi//MREREAAAAAAAAzIiIiIiIiIiIiIiIiIzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMhEREREREAAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "bluetooth": {name: /*LANG*/"Bluetooth", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "outdoors": {name: /*LANG*/"Outdoor", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "misc": {name: /*LANG*/"Misc", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "health": {name: /*LANG*/"Health", icon: () => require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA=="))},
};
// handle customised launcher
@@ -73,6 +72,23 @@ let tagKeys = Object.keys(tags).filter(tag => tag !== "clock" || settings.showCl
if (!settings.fullscreen)
Bangle.loadWidgets();
+const unload = () => {
+ // cleanup the timeout to not leave anything behind after being removed from ram
+ if (lockTimeout) clearTimeout(lockTimeout);
+ Bangle.removeListener("lock", lockHandler);
+};
+
+// 10s of inactivity goes back to clock
+Bangle.setLocked(false); // unlock initially
+let lockTimeout;
+let lockHandler = function(locked) {
+ if (lockTimeout) clearTimeout(lockTimeout);
+ lockTimeout = undefined;
+ if (locked) {
+ lockTimeout = setTimeout(Bangle.showClock, 10000);
+ }
+};
+
let showTagMenu = (tag) => {
E.showScroller({
h : 64*scaleval, c : appsByTag[tag].length,
@@ -96,8 +112,10 @@ let showTagMenu = (tag) => {
load(app.src);
}
},
- back : showMainMenu
+ back : showMainMenu,
+ remove: unload
});
+ Bangle.on("lock", lockHandler);
};
let showMainMenu = () => {
@@ -108,7 +126,7 @@ let showMainMenu = () => {
g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1));
g.setFont(font).setFontAlign(-1,0).drawString(tags[tag].name,64*scaleval,r.y+(32*scaleval));
- const img = tags[tag].icon ? tags[tag].icon : s.read("taglaunch." + tag + ".img");
+ const img = tags[tag].icon ? tags[tag].icon() : s.read("taglaunch." + tag + ".img");
if (img) {
try {g.drawImage(img,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){}
}
@@ -118,27 +136,13 @@ let showMainMenu = () => {
showTagMenu(tag);
},
back : Bangle.showClock, // button press or tap in top left shows clock now
- remove : () => {
- // cleanup the timeout to not leave anything behind after being removed from ram
- if (lockTimeout) clearTimeout(lockTimeout);
- Bangle.removeListener("lock", lockHandler);
- }
+ remove : unload
});
+ Bangle.on("lock", lockHandler);
};
showMainMenu();
g.flip(); // force a render before widgets have finished drawing
-// 10s of inactivity goes back to clock
-Bangle.setLocked(false); // unlock initially
-let lockTimeout;
-let lockHandler = function(locked) {
- if (lockTimeout) clearTimeout(lockTimeout);
- lockTimeout = undefined;
- if (locked) {
- lockTimeout = setTimeout(Bangle.showClock, 10000);
- }
-};
-Bangle.on("lock", lockHandler);
if (!settings.fullscreen) // finally draw widgets
Bangle.drawWidgets();
}
diff --git a/apps/taglaunch/health-icon.js b/apps/taglaunch/health-icon.js
deleted file mode 100644
index 11b513b72..000000000
--- a/apps/taglaunch/health-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA=="))
diff --git a/apps/taglaunch/metadata.json b/apps/taglaunch/metadata.json
index 4f7c295e9..a4fb4ef6c 100644
--- a/apps/taglaunch/metadata.json
+++ b/apps/taglaunch/metadata.json
@@ -2,7 +2,7 @@
"id": "taglaunch",
"name": "Tag Launcher",
"shortName": "Taglauncher",
- "version": "0.03",
+ "version": "0.04",
"description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.",
"readme": "README.md",
"icon": "app.png",
@@ -12,8 +12,7 @@
"screenshots": [ {"url":"screenshot.png"} ],
"storage": [
{"name":"taglaunch.app.js","url":"app.js"},
- {"name":"taglaunch.settings.js","url":"settings.js"},
- {"name":"taglaunch.health.img","url":"health-icon.js","evaluate":true}
+ {"name":"taglaunch.settings.js","url":"settings.js"}
],
"data": [{"name":"taglaunch.json"},{"name":"taglaunch.cache.json"}]
}
diff --git a/apps/timerclk/ChangeLog b/apps/timerclk/ChangeLog
index 5a954d58c..46aa52ee1 100644
--- a/apps/timerclk/ChangeLog
+++ b/apps/timerclk/ChangeLog
@@ -2,3 +2,4 @@
0.02: Add sunrise/sunset. Fix timer bugs.
0.03: Use default Bangle formatter for booleans
0.04: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
+0.05: Improve responsiveness and detection of swipes on main clock screen
diff --git a/apps/timerclk/app.js b/apps/timerclk/app.js
index ee30b059a..e489f9844 100644
--- a/apps/timerclk/app.js
+++ b/apps/timerclk/app.js
@@ -148,23 +148,23 @@ if (process.env.HWVERSION==1) {
setWatch(()=>load("timerclk.alarm.js"), BTN3);
setWatch(()=>load("timerclk.alarm.js"), BTN1);
} else {
- var absY, lastX, lastY;
+ var absY, lastX=0, lastY=0;
Bangle.on('drag', e=>{
if (!e.b) {
- if (lastX > 50) { // right
+ if (lastX > 5) { // right
if (absY < dragBorder) { // drag over time
load("timerclk.timer.js");
}else { // drag over date/dow
load("timerclk.alarm.js");
}
- } else if (lastX < -50) { // left
+ } else if (lastX < -5) { // left
if (absY < dragBorder) { // drag over time
load("timerclk.stopwatch.js");
}else { // drag over date/dow
load("timerclk.alarm.js");
}
- } else if (lastY > 50) { // down
- } else if (lastY < -50) { // up
+ } else if (lastY > 5) { // down
+ } else if (lastY < -5) { // up
}
lastX = 0;
lastY = 0;
diff --git a/apps/timerclk/lib.js b/apps/timerclk/lib.js
index dd3893fa1..47f49736f 100644
--- a/apps/timerclk/lib.js
+++ b/apps/timerclk/lib.js
@@ -87,7 +87,7 @@ exports.registerControls = function(o) {
}
}
});
- var absX, lastX, lastY;
+ var absX, lastX=0, lastY=0;
Bangle.on('drag', e=>{
if (!e.b) {
if (lastX > 40) { // right
diff --git a/apps/timerclk/metadata.json b/apps/timerclk/metadata.json
index 5bd6bee24..0a6311ac1 100644
--- a/apps/timerclk/metadata.json
+++ b/apps/timerclk/metadata.json
@@ -2,7 +2,7 @@
"id": "timerclk",
"name": "Timer Clock",
"shortName":"Timer Clock",
- "version":"0.04",
+ "version":"0.05",
"description": "A clock with stopwatches, timers and alarms build in.",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog
index 279fa2438..7d6707467 100644
--- a/apps/wid_edit/ChangeLog
+++ b/apps/wid_edit/ChangeLog
@@ -3,3 +3,5 @@
Change back entry to menu option
Allow changing widgets into all areas, including bottom widget bar
0.03: Fix editing widgets whose draw method takes the widget
+0.04: Remove double-sort
+0.05: Restore alphabetical sort
diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js
index 3cb545a34..fe259f97f 100644
--- a/apps/wid_edit/boot.js
+++ b/apps/wid_edit/boot.js
@@ -14,7 +14,7 @@ Bangle.loadWidgets = (o => ()=>{
const W = global.WIDGETS;
global.WIDGETS = {};
Object.keys(W)
- .sort()
+ .sort() // sort alphabetically. the next sort is stable and preserves this if sortorder matches
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
.forEach(k => global.WIDGETS[k] = W[k]);
})(Bangle.loadWidgets);
diff --git a/apps/wid_edit/metadata.json b/apps/wid_edit/metadata.json
index e80e45d45..b89640333 100644
--- a/apps/wid_edit/metadata.json
+++ b/apps/wid_edit/metadata.json
@@ -1,6 +1,6 @@
{
"id": "wid_edit",
- "version": "0.03",
+ "version": "0.05",
"name": "Widget Editor",
"icon": "icon.png",
"description": "Customize widget locations",
diff --git a/apps/wid_edit/settings.js b/apps/wid_edit/settings.js
index be09923f2..a632850d6 100644
--- a/apps/wid_edit/settings.js
+++ b/apps/wid_edit/settings.js
@@ -27,7 +27,7 @@
let W = global.WIDGETS;
global.WIDGETS = {};
Object.keys(W)
- .sort()
+ .sort() // see comment in boot.js
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
.forEach(k => {global.WIDGETS[k] = W[k];});
Bangle.drawWidgets();
diff --git a/apps/widanclk/ChangeLog b/apps/widanclk/ChangeLog
index 337288ad2..5a3ba7b14 100644
--- a/apps/widanclk/ChangeLog
+++ b/apps/widanclk/ChangeLog
@@ -1,2 +1,3 @@
0.01: New app
0.02: Clear between redraws
+0.03: Add todays date behind the clock hands.
diff --git a/apps/widanclk/metadata.json b/apps/widanclk/metadata.json
index cd9347601..39e83a8fe 100644
--- a/apps/widanclk/metadata.json
+++ b/apps/widanclk/metadata.json
@@ -1,8 +1,8 @@
{
"id": "widanclk",
"name": "Analog clock widget",
- "version": "0.02",
- "description": "A simple analog clock widget that appears when not showing a fullscreen clock",
+ "version": "0.03",
+ "description": "A simple analog clock widget that appears when not showing a fullscreen clock. Todays date sits behind the clock hands.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,clock",
diff --git a/apps/widanclk/widget.js b/apps/widanclk/widget.js
index c58f56459..da667d29b 100644
--- a/apps/widanclk/widget.js
+++ b/apps/widanclk/widget.js
@@ -9,10 +9,15 @@ WIDGETS["wdanclk"]={area:"tl",width:Bangle.CLOCK?0:24,draw:function() {
if (!this.width) return; // if size not right, return
g.reset();
let d = new Date();
+ let dd = d.getDate();
let x=this.x+12, y=this.y+12,
ah = (d.getHours()+d.getMinutes()/60)*Math.PI/6,
am = d.getMinutes()*Math.PI/30;
g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23).
+ setFont("Vector:16").
+ setColor(g.theme.bgH).
+ drawString(dd,this.x+4+10*(dd<10),this.y+5,true).
+ setColor(g.theme.fg).
drawCircle(x, y, 11).
drawLine(x,y, x+Math.sin(ah)*7, y-Math.cos(ah)*7).
drawLine(x,y, x+Math.sin(am)*9, y-Math.cos(am)*9);
diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog
index 3b2ae75c4..e9adf252d 100644
--- a/apps/widbaroalarm/ChangeLog
+++ b/apps/widbaroalarm/ChangeLog
@@ -8,3 +8,4 @@
Only use valid pressure values
0.06: Fix exception
0.07: Ensure barometer gets turned off after a few readings (isBarometerOn broken in 2v16)
+0.08: Compatibility with hideable Widgets
diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json
index 0a6ddd71e..ba0c02a31 100644
--- a/apps/widbaroalarm/metadata.json
+++ b/apps/widbaroalarm/metadata.json
@@ -2,7 +2,7 @@
"id": "widbaroalarm",
"name": "Barometer Alarm Widget",
"shortName": "Barometer Alarm",
- "version": "0.07",
+ "version": "0.08",
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
"icon": "widget.png",
"type": "widget",
diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js
index d65a1c09c..c7ca0eda2 100644
--- a/apps/widbaroalarm/widget.js
+++ b/apps/widbaroalarm/widget.js
@@ -226,7 +226,7 @@ function barometerPressureHandler(e) {
medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9);
if (medianPressure > 0) {
turnOff();
- draw();
+ WIDGETS.baroalarm.draw();
handlePressureValue(medianPressure);
}
}
@@ -253,13 +253,6 @@ function turnOff() {
}
function draw() {
- if (global.WIDGETS != undefined && typeof global.WIDGETS === "object") {
- global.WIDGETS["baroalarm"] = {
- width : setting("show") ? 24 : 0,
- area : "tr",
- draw : draw
- };
- }
g.reset();
if (this.x == undefined || this.y != 0)
@@ -270,9 +263,6 @@ function draw() {
if (setting("show")) {
g.setFont("6x8", 1).setFontAlign(1, 0);
if (medianPressure == undefined) {
- // trigger a new check
- getPressureValue();
-
// lets load last value from log (if available)
if (history3.length > 0) {
medianPressure = history3[history3.length - 1]["p"];
@@ -297,6 +287,12 @@ function draw() {
}
}
+WIDGETS["baroalarm"] = {
+ width : setting("show") ? 24 : 0,
+ area : "tr",
+ draw : draw
+};
+
if (interval > 0) {
setInterval(getPressureValue, interval * 60000);
}
diff --git a/apps/widclkscrl/metadata.json b/apps/widclkscrl/metadata.json
new file mode 100644
index 000000000..81221cbe4
--- /dev/null
+++ b/apps/widclkscrl/metadata.json
@@ -0,0 +1,13 @@
+{
+ "id": "widclkscrl",
+ "name": "Scrolling clock widget",
+ "version": "0.01",
+ "description": "A widget that displays the current date & time after unlocking the watch when not showing a fullscreen clock. The information is scrolled by in a two digit field, so this widget is kept tight.",
+ "icon": "widget.png",
+ "type": "widget",
+ "tags": "widget",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"widclkscrl.wid.js","url":"widget.js"}
+ ]
+}
diff --git a/apps/widclkscrl/widget.js b/apps/widclkscrl/widget.js
new file mode 100644
index 000000000..292d291ac
--- /dev/null
+++ b/apps/widclkscrl/widget.js
@@ -0,0 +1,64 @@
+(() => {
+ const WIDTH = 14; // Width of the text, widget is +2 px wide
+ const CONTINOUS = false; // Go back & forward or stop after first scroll
+ require("FontTeletext5x9Ascii").add(Graphics);
+
+ function getDateText() {
+ const date = new Date();
+ const dateStr = require("locale").date(date, 1);
+ const timeStr = require("locale").time(date, 1);
+ return ` ${timeStr} ${dateStr} `;
+ }
+
+ WIDGETS["widclkscrl"]={
+ area: "tl",
+ width: 0, // default hide
+ pos: 10,
+ dir: -1,
+ eventHandlerSet: false,
+ draw: function() {
+ if (!this.eventHandlerSet) {
+ Bangle.on('lock', (on) => {
+ this.run(!on);
+ });
+ this.eventHandlerSet = true;
+ }
+ if (this.text) {
+ const buf = Graphics.createArrayBuffer(WIDTH,24,1,{msb:true}).setFont("Teletext5x9Ascii:1x2").setFontAlign(-1, 0);
+ buf.drawString(this.text, this.pos, 12);
+
+ if (this.dir === 1 && this.pos === 0 || this.dir === -1 && Math.abs(this.pos) === buf.stringWidth(this.text) - WIDTH) {
+ if (CONTINOUS) {
+ this.dir*=-1;
+ this.text = getDateText();
+ } else {
+ this.pos = 0;
+ this.run(false);
+ return;
+ }
+ }
+ this.pos+=this.dir;
+
+ g.reset().drawImage({
+ width:buf.getWidth(), height:buf.getHeight(),
+ bpp:buf.getBPP(),
+ buffer:buf.buffer
+ }, this.x+1, this.y);
+ }
+ },
+ run: function (on) {
+ if (!Bangle.CLOCK && on && !this.interval) {
+ this.text = getDateText();
+ this.interval = setInterval(() => {
+ this.draw();
+ }, 100);
+ this.width = WIDTH+2; Bangle.drawWidgets();
+ } else if (!on && this.interval) {
+ clearInterval(this.interval);
+ delete this.interval;
+ delete this.text;
+ this.width = 0; Bangle.drawWidgets();
+ }
+ },
+ };
+})();
diff --git a/apps/widclkscrl/widget.png b/apps/widclkscrl/widget.png
new file mode 100644
index 000000000..6b4bc9774
Binary files /dev/null and b/apps/widclkscrl/widget.png differ
diff --git a/apps/widshipbell/ChangeLog b/apps/widshipbell/ChangeLog
new file mode 100644
index 000000000..a26ed96db
--- /dev/null
+++ b/apps/widshipbell/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called
diff --git a/apps/widshipbell/metadata.json b/apps/widshipbell/metadata.json
index c130b04ee..1c4a7613e 100644
--- a/apps/widshipbell/metadata.json
+++ b/apps/widshipbell/metadata.json
@@ -2,7 +2,7 @@
"id": "widshipbell",
"name": "Ship's bell Widget",
"shortName": "Ship's bell",
- "version": "0.01",
+ "version": "0.02",
"description": "A widget that buzzes according to a nautical bell, one strike at 04:30, two strikes at 05:00, up to eight strikes at 08:00 and so on.",
"icon": "widget.png",
"type": "widget",
diff --git a/core b/core
index 92769acd6..b8813ab92 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 92769acd60bc31548ff7c635128d4e7ef02b7325
+Subproject commit b8813ab92ceb70fb8ec6a7de6baaec88f6b5026f
diff --git a/index.html b/index.html
index 30f660717..6c3809343 100644
--- a/index.html
+++ b/index.html
@@ -135,15 +135,17 @@
Utilities
+
+
+
+
+
-
-
-
Settings
@@ -175,6 +177,10 @@
Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this will break many apps)
+
diff --git a/lang/de_DE.json b/lang/de_DE.json
index 6c2204cf1..9f9af6b18 100644
--- a/lang/de_DE.json
+++ b/lang/de_DE.json
@@ -53,6 +53,13 @@
"Delete all messages": "Alle Nachrichten löschen",
"Unread timer": "Ungelesener Timer",
"Quiet Mode": "Stiller Modus",
+ "Silent": "Still",
+ "Current Mode": "Aktueller Modus",
+ "Add Schedule": "Neue Aktion",
+ "Switch Theme": "Theme ändern",
+ "Options": "Optionen",
+ "Normal Theme": "Normales Theme",
+ "Quiet Theme": "Stilles Theme",
"Utils": "Werkzeuge",
"Piezo": "Piezo",
"LCD": "LCD",
diff --git a/package-lock.json b/package-lock.json
index eb7270554..c7cf318f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2146,9 +2146,9 @@
}
},
"node_modules/word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -3739,9 +3739,9 @@
}
},
"word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrappy": {
diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts
index ecf509f70..eda8a9a81 100644
--- a/typescript/types/main.d.ts
+++ b/typescript/types/main.d.ts
@@ -180,6 +180,7 @@ type SetUIArg = Mode | {
mode: Mode,
back?: () => void,
remove?: () => void,
+ redraw?: () => void,
};
type NRFFilters = {
@@ -841,6 +842,16 @@ declare class NRF {
*/
static on(event: "disconnect", callback: (reason: number) => void): void;
+ /**
+ * Called when the Nordic Bluetooth stack (softdevice) generates an error. In pretty
+ * much all cases an Exception will also have been thrown.
+ * @param {string} event - The event to listen to.
+ * @param {(msg: any) => void} callback - A function that is executed when the event occurs. Its arguments are:
+ * * `msg` The error string
+ * @url http://www.espruino.com/Reference#l_NRF_error
+ */
+ static on(event: "error", callback: (msg: any) => void): void;
+
/**
* Contains updates on the security of the current Bluetooth link.
* See Nordic's `ble_gap_evt_auth_status_t` structure for more information.
@@ -964,6 +975,17 @@ declare class NRF {
*/
static restart(callback?: any): void;
+ /**
+ * Delete all data stored for all peers (bonding data used for secure connections). This cannot be done
+ * while a connection is active, so if there is a connection it will be postponed until everything is disconnected
+ * (which can be done by calling `NRF.disconnect()` and waiting).
+ * Booting your device while holding all buttons down together should also have the same effect.
+ *
+ * @param {any} [callback] - [optional] A function to be called while the softdevice is uninitialised. Use with caution - accessing console/bluetooth will almost certainly result in a crash.
+ * @url http://www.espruino.com/Reference#l_NRF_eraseBonds
+ */
+ static eraseBonds(callback?: any): void;
+
/**
* Get this device's default Bluetooth MAC address.
* For Puck.js, the last 5 characters of this (e.g. `ee:ff`) are used in the
@@ -1222,8 +1244,9 @@ declare class NRF {
* hid : new Uint8Array(...), // optional, default is undefined. Enable BLE HID support
* uart : true, // optional, default is true. Enable BLE UART support
* advertise: [ '180D' ] // optional, list of service UUIDs to advertise
- * ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications
- * ams : true // optional, Bangle.js-only, enable Apple AMS support for media control
+ * ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications (see `NRF.ancs*`)
+ * ams : true // optional, Bangle.js-only, enable Apple AMS support for media control (see `NRF.ams*`)
+ * cts : true // optional, Bangle.js-only, enable Apple Current Time Service support (see `NRF.ctsGetTime`)
* });
* ```
* To enable BLE HID, you must set `hid` to an array which is the BLE report
@@ -1747,6 +1770,55 @@ declare class NRF {
*/
static amsCommand(id: any): void;
+ /**
+ * Check if Apple Current Time Service (CTS) is currently active on the BLE connection
+ *
+ * @returns {boolean} True if Apple Current Time Service (CTS) has been initialised and is active
+ * @url http://www.espruino.com/Reference#l_NRF_ctsIsActive
+ */
+ static ctsIsActive(): boolean;
+
+ /**
+ * Returns time information from the Current Time Service
+ * (if requested with `NRF.ctsGetTime` and is activated by calling `NRF.setServices(..., {..., cts:true})`)
+ * ```
+ * {
+ * date : // Date object with the current date
+ * day : // if known, 0=sun,1=mon (matches JS `Date`)
+ * reason : [ // reason for the date change
+ * "external", // External time change
+ * "manual", // Manual update
+ * "timezone", // Timezone changed
+ * "DST", // Daylight savings
+ * ]
+ * timezone // if LTI characteristic exists, this is the timezone
+ * dst // if LTI characteristic exists, this is the dst adjustment
+ * }
+ * ```
+ * For instance this can be used as follows to update Espruino's time:
+ * ```
+ * E.on('CTS',e=>{
+ * setTime(e.date.getTime()/1000);
+ * });
+ * NRF.ctsGetTime(); // also returns a promise with CTS info
+ * ```
+ * @param {string} event - The event to listen to.
+ * @param {(info: any) => void} callback - A function that is executed when the event occurs. Its arguments are:
+ * * `info` An object (see below)
+ * @url http://www.espruino.com/Reference#l_NRF_CTS
+ */
+ static on(event: "CTS", callback: (info: any) => void): void;
+
+ /**
+ * Read the time from CTS - creates an `NRF.on('CTS', ...)` event as well
+ * ```
+ * NRF.ctsGetTime(); // also returns a promise
+ * ```
+ * @returns {any} A `Promise` that is resolved (or rejected) when time is received
+ * @url http://www.espruino.com/Reference#l_NRF_ctsGetTime
+ */
+ static ctsGetTime(): Promise;
+
/**
* Search for available devices matching the given filters. Since we have no UI
* here, Espruino will pick the FIRST device it finds, or it'll call `catch`.
@@ -3538,6 +3610,16 @@ declare class Bangle {
*/
static on(event: "lcdPower", callback: (on: boolean) => void): void;
+ /**
+ * Has the backlight been turned on or off? Can be used to stop tasks that are no
+ * longer useful if want to see in sun screen only. Also see `Bangle.isBacklightOn()`
+ * @param {string} event - The event to listen to.
+ * @param {(on: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are:
+ * * `on` `true` if backlight is on
+ * @url http://www.espruino.com/Reference#l_Bangle_backlight
+ */
+ static on(event: "backlight", callback: (on: boolean) => void): void;
+
/**
* Has the screen been locked? Also see `Bangle.isLocked()`
* @param {string} event - The event to listen to.
@@ -3664,6 +3746,26 @@ declare class Bangle {
*/
static on(event: "midnight", callback: () => void): void;
+ /**
+ * This function can be used to turn Bangle.js's LCD backlight off or on.
+ * This function resets the Bangle's 'activity timer' (like pressing a button or
+ * the screen would) so after a time period of inactivity set by
+ * `Bangle.setOptions({backlightTimeout: X});` the backlight will turn off.
+ * If you want to keep the backlight on permanently (until apps are changed) you can
+ * do:
+ * ```
+ * Bangle.setOptions({backlightTimeout: 0}) // turn off the timeout
+ * Bangle.setBacklight(1); // keep screen on
+ * ```
+ * Of course, the backlight depends on `Bangle.setLCDPower` too, so any lcdPowerTimeout/setLCDTimeout will
+ * also turn the backlight off. The use case is when you require the backlight timeout
+ * to be shorter than the power timeout.
+ *
+ * @param {boolean} isOn - True if the LCD backlight should be on, false if not
+ * @url http://www.espruino.com/Reference#l_Bangle_setBacklight
+ */
+ static setBacklight(isOn: boolean): void;
+
/**
* This function can be used to turn Bangle.js's LCD off or on.
* This function resets the Bangle's 'activity timer' (like pressing a button or
@@ -3805,7 +3907,7 @@ declare class Bangle {
static setLCDTimeout(isOn: number): void;
/**
- * Set how often the watch should poll for new acceleration/gyro data and kick the
+ * Set how often the watch should poll its sensors (accel/hr/mag) for new data and kick the
* Watchdog timer. It isn't recommended that you make this interval much larger
* than 1000ms, but values up to 4000ms are allowed.
* Calling this will set `Bangle.setOptions({powerSave: false})` - disabling the
@@ -3890,6 +3992,13 @@ declare class Bangle {
*/
static isLCDOn(): boolean;
+ /**
+ * Also see the `Bangle.backlight` event
+ * @returns {boolean} Is the backlight on or not?
+ * @url http://www.espruino.com/Reference#l_Bangle_isBacklightOn
+ */
+ static isBacklightOn(): boolean;
+
/**
* This function can be used to lock or unlock Bangle.js (e.g. whether buttons and
* touchscreen work or not)
@@ -4361,7 +4470,8 @@ declare class Bangle {
* (function() {
* var sui = Bangle.setUI;
* Bangle.setUI = function(mode, cb) {
- * if (mode!="clock") return sui(mode,cb);
+ * var m = ("object"==typeof mode) ? mode.mode : mode;
+ * if (m!="clock") return sui(mode,cb);
* sui(); // clear
* Bangle.CLOCK=1;
* Bangle.swipeHandler = Bangle.showLauncher;
@@ -4376,10 +4486,11 @@ declare class Bangle {
* mode : "custom",
* back : function() {}, // optional - add a 'back' icon in top-left widget area and call this function when it is pressed , also call it when the hardware button is clicked (does not override btn if defined)
* remove : function() {}, // optional - add a handler for when the UI should be removed (eg stop any intervals/timers here)
- * touch : function(n,e) {}, // optional - handler for 'touch' events
- * swipe : function(dir) {}, // optional - handler for 'swipe' events
- * drag : function(e) {}, // optional - handler for 'drag' events (Bangle.js 2 only)
- * btn : function(n) {}, // optional - handler for 'button' events (n==1 on Bangle.js 2, n==1/2/3 depending on button for Bangle.js 1)
+ * redraw : function() {}, // optional - add a handler to redraw the UI. Not needed but it can allow widgets/etc to provide other functionality that requires the screen to be redrawn
+ * touch : function(n,e) {}, // optional - (mode:custom only) handler for 'touch' events
+ * swipe : function(dir) {}, // optional - (mode:custom only) handler for 'swipe' events
+ * drag : function(e) {}, // optional - (mode:custom only) handler for 'drag' events (Bangle.js 2 only)
+ * btn : function(n) {}, // optional - (mode:custom only) handler for 'button' events (n==1 on Bangle.js 2, n==1/2/3 depending on button for Bangle.js 1)
* clock : 0 // optional - if set the behavior of 'clock' mode is added (does not override btn if defined)
* });
* ```
@@ -4387,7 +4498,7 @@ declare class Bangle {
* may choose to just call the `remove` function and then load a new app without resetting Bangle.js.
* As a result, **if you specify 'remove' you should make sure you test that after calling `Bangle.setUI()`
* without arguments your app is completely unloaded**, otherwise you may end up with memory leaks or
- * other issues when switching apps.
+ * other issues when switching apps. Please see http://www.espruino.com/Bangle.js+Fast+Load for more details on this.
*
* @param {any} type - The type of UI input: 'updown', 'leftright', 'clock', 'clockupdown' or undefined to cancel. Can also be an object (see below)
* @param {any} callback - A function with one argument which is the direction
@@ -6872,6 +6983,7 @@ interface DateConstructor {
new(): Date;
new(value: number | string): Date;
new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date;
+ (arg?: any): string;
}
interface Date {
@@ -8166,7 +8278,9 @@ declare class E {
static toFlatString(...args: any[]): string | undefined;
/**
- * By default, strings in Espruino are standard 8 bit binary strings.
+ * By default, strings in Espruino are standard 8 bit binary strings
+ * unless they contain Unicode chars or a `\u####` escape code
+ * that doesn't map to the range 0..255.
* However calling E.asUTF8 will convert one of those strings to
* UTF8.
* ```
@@ -8177,6 +8291,7 @@ declare class E {
* u.length // 1
* u[0] // hamburger emoji
* ```
+ * **NOTE:** UTF8 is currently only available on Bangle.js devices
*
* @param {any} str - The string to turn into a UTF8 Unicode String
* @returns {any} A String
@@ -8184,6 +8299,34 @@ declare class E {
*/
static asUTF8(str: any): string;
+ /**
+ * Given a UTF8 String (see `E.asUTF8`) this returns the underlying representation
+ * of that String.
+ * ```
+ * E.fromUTF8("\u03C0") == "\xCF\x80"
+ * ```
+ * **NOTE:** UTF8 is currently only available on Bangle.js devices
+ *
+ * @param {any} str - The string to check
+ * @returns {any} A String
+ * @url http://www.espruino.com/Reference#l_E_fromUTF8
+ */
+ static fromUTF8(str: any): string;
+
+ /**
+ * By default, strings in Espruino are standard 8 bit binary strings
+ * unless they contain Unicode chars or a `\u####` escape code
+ * that doesn't map to the range 0..255.
+ * This checks if a String is being treated by Espruino as a UTF8 String
+ * See `E.asUTF8` to convert to a UTF8 String
+ * **NOTE:** UTF8 is currently only available on Bangle.js devices
+ *
+ * @param {any} str - The string to check
+ * @returns {boolean} True if the given String is treated as UTF8 by Espruino
+ * @url http://www.espruino.com/Reference#l_E_isUTF8
+ */
+ static isUTF8(str: any): boolean;
+
/**
* This creates a Uint8Array from the given arguments. These are handled as
* follows:
@@ -9120,6 +9263,26 @@ interface Object {
*/
on(event: any, listener: any): void;
+ /**
+ * Register an event listener for this object, for instance `Serial1.addListener('data', function(d) {...})`.
+ * An alias for `Object.on`
+ *
+ * @param {any} event - The name of the event, for instance 'data'
+ * @param {any} listener - The listener to call when this event is received
+ * @url http://www.espruino.com/Reference#l_Object_addListener
+ */
+ addListener(event: any, listener: any): void;
+
+ /**
+ * Register an event listener for this object, for instance `Serial1.addListener('data', function(d) {...})`.
+ * An alias for `Object.on`
+ *
+ * @param {any} event - The name of the event, for instance 'data'
+ * @param {any} listener - The listener to call when this event is received
+ * @url http://www.espruino.com/Reference#l_Object_prependListener
+ */
+ prependListener(event: any, listener: any): void;
+
/**
* Call any event listeners that were added to this object with `Object.on`, for
* instance `obj.emit('data', 'Foo')`.
@@ -9807,21 +9970,22 @@ declare const Promise: PromiseConstructor
* This means that while `StorageFile` files exist in the same area as those from
* `Storage`, they should be read using `Storage.open` (and not `Storage.read`).
* ```
- * f = s.open("foobar","w");
+ * f = require("Storage").open("foobar","w");
* f.write("Hell");
* f.write("o World\n");
* f.write("Hello\n");
* f.write("World 2\n");
+ * f.write("Hello World 3\n");
* // there's no need to call 'close'
* // then
- * f = s.open("foobar","r");
+ * f = require("Storage").open("foobar","r");
* f.read(13) // "Hello World\nH"
* f.read(13) // "ello\nWorld 2\n"
* f.read(13) // "Hello World 3"
* f.read(13) // "\n"
* f.read(13) // undefined
* // or
- * f = s.open("foobar","r");
+ * f = require("Storage").open("foobar","r");
* f.readLine() // "Hello World\n"
* f.readLine() // "Hello\n"
* f.readLine() // "World 2\n"
@@ -10255,6 +10419,7 @@ interface StringConstructor {
* @url http://www.espruino.com/Reference#l_String_String
*/
new(...str: any[]): any;
+ (arg?: any): string;
}
interface String {
@@ -10499,7 +10664,8 @@ interface RegExpConstructor {
* @returns {any} A RegExp object
* @url http://www.espruino.com/Reference#l_RegExp_RegExp
*/
- new(regex: any, flags: any): RegExp;
+ new(...value: any[]): RegExp;
+ (value: any): RegExp;
}
interface RegExp {
@@ -10588,7 +10754,8 @@ interface NumberConstructor {
* @returns {any} A Number object
* @url http://www.espruino.com/Reference#l_Number_Number
*/
- new(...value: any[]): any;
+ new(...value: any[]): Number;
+ (value: any): number;
}
interface Number {
@@ -10728,7 +10895,8 @@ interface BooleanConstructor {
* @returns {boolean} A Boolean object
* @url http://www.espruino.com/Reference#l_Boolean_Boolean
*/
- new(value: any): boolean;
+ new(...value: any[]): Number;
+ (value: any): boolean;
}
interface Boolean {
@@ -13494,7 +13662,7 @@ declare module "Storage" {
* List all files in the flash storage area matching the specified regex (ignores
* StorageFiles), and then hash their filenames *and* file locations.
* Identical files may have different hashes (e.g. if Storage is compacted and the
- * file moves) but the changes of different files having the same hash are
+ * file moves) but the chances of different files having the same hash are
* extremely small.
* ```
* // Hash files