Merge pull request #3108 from g-rden/dwm-clock

Daylight world map clock
master
Rob Pilling 2023-11-22 08:00:49 +00:00
commit 0e4fa182eb
7 changed files with 252 additions and 0 deletions

1
apps/dwm-clock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

7
apps/dwm-clock/README.md Normal file
View File

@ -0,0 +1,7 @@
A clock with a daylight world map
The function for the daylight graph is a crude approximation for an equirectangular projection of a circle on a sphere.
You can change the longitudinal map offset by swiping the map sideways. For saving the changes to the file dwm-clock.json press the top left quarter of the screen. To discard changes press the top right quarter.
If you are interested in changing the vector font to another one, please do.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4n/AoPg/8fAYM5sGt4FakFjsFKgHGv9rsEpoHOkGl0ExsHvj0AkQAugHyAYMv/4ECkX/BwfwAogACgQDCl8PBQnwBQZ3BC4opDAAg0BGIP/+CVCAoPykAiBiIABjOZBwIMB/8PUYQAJ4AYCiuZzLLQCwUYgEEgGZuAXQ5bEBqMAgeTChaHCiMc2cRmFTBQMJ0AXMl8Rj0QqALEyAwL+QXBGAIKFhJHOjcMglz4EMjew4IwJWILVBiMWPQNOstABgMBsxFJkAvCiNsmc+AgPqSIJIIe4KPEBAMG9FstQFCJA/y/4WBC4PNBo0IhQXICwUijy3FglggtKjIXMBYsKwAaBjPwRIICBIgYXJgGFgHBiMvZ4QtCgQXLhXOSgOfQ4TTCAoR3CuAYGhgXBySiBCgIvB+AXEisA8AXEgIXBiUvkUvLwP/AoIXDAATtCAAPhC4QOBL4Mvh//SAYXHhwHCjIPC+Hwh8CkCTCC4cWFwoXEJAIUCPgPyC4/RBAeSLIZiCAYIvIgO0C45LCI5WLR4RIEAAMgIwLaBC4cbCwMIigXI/4lCbQIXDiLVCjdhMA7NEO4kRmEACogXF+AXDgQOEiEGCwrBFC5MeXoYXHl7uBDQJHFABKoFYQQXOYIzXFABUSC4rXFABTZFC6JIHC6EZDAwXPJI4PIilBAgdEAAOZz//UwIXCj3r3YABxGwcgVLwDqDgF5zOQgEPj3QBYgANhOZzMwCyQA/ABYA="))

224
apps/dwm-clock/app.js Normal file
View File

@ -0,0 +1,224 @@
// daylight world map clock
// equirectangular projected map and approximated daylight graph
// load font for timezone, weekday and day in month
require("FontDennis8").add(Graphics);
const W = g.getWidth();
const H = g.getHeight();
const TZOFFSET = new Date().getTimezoneOffset();
const UTCSTRING = ((TZOFFSET > 0 ? "-" : "+")
+ ("0" + Math.floor(Math.abs(TZOFFSET) / 60)).slice(-2))
+ (TZOFFSET % 60 ? Math.abs(TZOFFSET) % 60 : "");
function getMap() {
return {
width: 176, height: 88, bpp: 1,
transparent: 1,
buffer: require("heatshrink").decompress(atob("/4A/AA0Av+Ag4UQwBhDn//1//8///AUI3MAhAUBgIQBh4LC/kfCg34rmAngVD/1/CYICBA4IAF8EOwF/+AVCAAXj//AA4PjDQIVDgkQj/4gBtEx+EGgXwCoJ8Bv+8geQgIVE4P/553Egf/nwFCgUE4H8gBqB/0AhLxHggFE+E8gJoBDIIAI5wFE4F8h/4v5FBABA2BAAUf7n+VYXgoAVNn/Dv+fCoPACo8MEQPHHAUf4DuB//58FgCgsHeoWfMgUDConw4AVFh/wXIRDDwBWC8jfBFY3xaAa5DYYXkKw8D+YVDHAcXAwKuIgIUDSIIJCsYVKeAIVHj5fGNogVHgN/AwPyEgPhCokZCo40D8E0wcwTYhsECoY0D8H2hEACocBCoqnCKwQVB/nICokJ+4VL/RGBQQkdw4VESQTwCDgIVBNgkeEQaSEQQReC4QrEhwUECoUECooAFVwoABgF+CoY+DAYZAFAAOgv4VGoFgCpXwGIoABkEHDQUvCo9zD4YVE4EIgIUGCoNnZwYVCiEP8E8hYVH/kHII0Qj/wvkP94WH4IVGhE/MQMH54VH+IVGKYIJBgfnCo/98IVFcYP5/9HMYbdGn7FFv/4/9vCpH/4DmC4AVCD4P/n4VKUoXgCwQ2Cz42CCpX//BtCCoMeCpJTBZgcAgYFCjElCpA7BEIQVBZoeYp4sICoIQCIIJzC/+Mp+DCpJSC/kAj4KC5/f4GfK5AVIeYPgNpIVEIIf/6f/v6ZHPwYVG//7V5BtDCoMOEof+jYVH8AVFhgLD/EZCo6UBCokYBYa2BCp04G4oVJNAX+gF4XYqDHCoKqCCoIrDAoL9DCowfCB4N9CorMDCooPEfowVMB4IVPeAQABwIVPeAQABw4LEg/ANo/wTAQAI8E//YVS+F//IIGGg4AFCo7OHAAf+v/jCowqM//HAwvhCpuPOwwVNAAwrOAA3xCqhtOAH4AfW4wAN/0/A4sP//AgFygYVH/V/AwlwgE8gAACDYIAF9ArC+uACAUgCocAHIn8k/gj4FBCgYAGBoXwgEYDof+ChMAJ4PmAwcBDgIUKgANBJIkZ/0cCpYrBIAIADzkwChQ5B/tgBAh7FNpANMAGg="))
};
}
const YOFFSET = H - getMap().height;
// map offset in degree
// -180 to 180 / default: 0
function getLongitudeOffset() {
return require("Storage").readJSON("dwm-clock.json", 1) || {"lon": 0};
}
function drawMap() {
g.setBgColor(0, 0, 0);
// does not flip on it's own, but there is a draw function after that does
g.drawImages([{
x: -lonOffset * W / 360,
y: YOFFSET,
image: getMap(),
scale: 1,
rotate: 0,
center: false,
repeat: true,
nobounds: false
}], {
x: 0,
y: YOFFSET,
width: getMap().width,
height: getMap().height
});
}
function drawDaylightMap() {
// number of xy points, < 40 looks very skewed around solstice
const STEPS = 40;
const YFACTOR = getMap().height / 2;
const YOFF = H / 2 + YFACTOR;
var graph = [];
// progress of day, float 0 to 1
var dayOffset = (now.getHours() + (now.getMinutes() + TZOFFSET) / 60) / 24;
// sun position modifier
var sunPosMod;
var solarNoon = require("suncalc").getTimes(now, 0, 0, 0).solarNoon;
var altitude = require("suncalc").getPosition(solarNoon, 0, 0).altitude;
// this is trial and error. no thought went into this
sunPosMod = Math.pow(altitude - 0.08, 8);
// switch sign on equinox
// this is an approximation
if (require("suncalc").getPosition(solarNoon, 0, 0).azimuth < -1) {
sunPosMod = -sunPosMod;
}
for (var x = 0; x < (STEPS + 1) / STEPS; x += 1 / STEPS) {
// this is an approximation instead of projecting a circle onto a sphere
// y = arctan(sin(x) * n)
var y = Math.atan(Math.sin(2 * Math.PI * x + dayOffset * 2 * Math.PI
// user defined map offset fixed offset
// v v
+ 2 * Math.PI * lonOffset / 360 - Math.PI / 2) * sunPosMod)
* (2 / Math.PI);
// ^
// factor keeps y <= 1
graph.push(x * W, y * YFACTOR + YOFF);
}
// day area, yellow
g.setColor(0.8, 0.8, 0.3);
g.fillRect(0, YOFFSET, W, H);
// night area, blue
g.setColor(0, 0, 0.5);
// switch on equinox
if (sunPosMod < 0) {
g.fillPoly([0, H - 1].concat(graph, W - 1, H - 1));
} else {
g.fillPoly([0, YOFFSET].concat(graph, W, YOFFSET));
}
drawMap();
// day-night line, white
g.setColor(1, 1, 1);
g.drawPoly(graph, false);
}
function drawClock() {
// clock area
g.clearRect(0, YOFFSET, W, 24);
// clock text
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.setFont("Vector", 58);
// with the vector font this leaves 26px above the text
g.drawString(require("locale").time(now, 1), W / 2, 24 - 2);
// timezone text
g.setFontAlign(-1, 1);
g.setFont("6x8", 2);
g.drawString("UTC" + UTCSTRING, 3, YOFFSET);
// day text
g.setFontAlign(1, 1);
g.setFont("Dennis8", 2);
g.drawString(require("locale").dow(now, 1) + " " + now.getDate(),
W - 1, YOFFSET);
}
function renderScreen() {
now = new Date();
drawClock();
drawDaylightMap();
}
function renderAndQueue() {
timeoutID = setTimeout(renderAndQueue, 60000 - (Date.now() % 60000));
renderScreen();
}
g.reset().clearRect(Bangle.appRect);
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
g.setBgColor(0, 0, 0);
var now = new Date();
// map offsets
var defLonOffset = getLongitudeOffset().lon;
var lonOffset = defLonOffset;
var timeoutID;
var timeoutIDTouch;
Bangle.on('drag', function(touch) {
if (timeoutIDTouch) {
clearTimeout(timeoutIDTouch);
}
// return after not touching for 5 seconds
timeoutIDTouch = setTimeout(renderAndQueue, 5 * 1000);
// touch map
if (touch.y >= YOFFSET) {
lonOffset -= touch.dx * 360 / W;
// wrap map offset
if (lonOffset < -180) {
lonOffset += 360;
} else if (lonOffset >= 180) {
lonOffset -= 360;
}
// snap to 0° longitude
if (lonOffset > -5 && lonOffset < 5) {
lonOffset = 0;
}
lonOffset = Math.round(lonOffset);
// clock area
g.clearRect(0, YOFFSET, W, 24);
// text
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.setFont("Dennis8", 2);
// could not get ° (degree sign) to render
g.drawString("select lon offset\n< tap: save\nreset: tap >\n"
+ lonOffset + " degree", W / 2, 24);
drawDaylightMap();
// touch clock, left side, save offset
} else if (touch.x < W / 2) {
if (defLonOffset != lonOffset) {
require("Storage").writeJSON("dwm-clock.json", {"lon": lonOffset});
defLonOffset = lonOffset;
}
renderScreen();
// touch clock, right side, reset offset
} else {
lonOffset = defLonOffset;
renderScreen();
}
});
renderAndQueue();

BIN
apps/dwm-clock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,19 @@
{
"id": "dwm-clock",
"name": "Daylight World Map Clock",
"shortName": "DWM Clock",
"version": "0.01",
"description": "A clock with a daylight world map",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"dwm-clock.app.js","url":"app.js"},
{"name":"dwm-clock.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"dwm-clock.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB