"If you torture the data for long enough, in the end they will confess". Ronald H. Coase (Nobel Prize in Economics in 1991)
martes, 1 de marzo de 2016
Open source Connect IQ face for Garmin watches - Analog & Skies (lunar phase and sunlight terminator)
Garmin empowers their watches by providing the Connect IQ SDK so that we can program our own apps, faces, etc. I wrote this open source (under GNU GPL v3.0) watch face which includes the calculations for the lunar phase and a sunlight terminator. It is space efficient and energy-efficient, because many of the calculations are only recomputed once a day (as opposed to the default every-minute), like the lunar phase or the sun declination. The sun declination is calculated in order to know the exact shape of the sunlight terminator, which is different, depending on the date. See, for example, the difference between summer and winter in the next two images:
The rest of the information displayed is explained in the following scheme:
The lunar phase and the sunlight terminator code are in two separate classes. They should be declared as class members, and initialized only once:
class AnalogBarsView extends Ui.WatchFace {
var moon, earth;
function initialize() {
WatchFace.initialize();
moon = new Moon(Ui.loadResource(Rez.Drawables.moon), 48, 18, 3);
earth = new SunlightTerminator(Ui.loadResource(Rez.Drawables.earth), 100, 50, 86, 20);
}
function onUpdate(dc) {
earth.updateable_sunlight_terminator(dc, time_sec, dateinfo, clockTime);
moon.updateable_calcmoonphase(dc, dateinfo, clockTime.hour);
// ...
}
}
These two calls in onUpdate will not recompute all their values on every call. Instead, they will conserve the old values and recalculate them every so often.
Here's Moon.mc:
using Toybox.Math as Math;
using Toybox.System as Sys;
using Toybox.WatchUi as Ui;
using Toybox.Graphics as Gfx;
// By using updateable_calcmoonphase, the moon phase picture will be drawn,
// but it will be only recomputed once a day.
class Moon {
var moon_width;
var moon_bitmap;
var moonx, moony;
var c_phase, t_phase; // day of month when last updated
var c_moon_label, c_moon_y; // y1, y2, y1, y2, ... for the moon shadow
function initialize(bitmap, width, x, y) {
moon_bitmap = bitmap; // Ui.loadResource(Rez.Drawables.moon);
moon_width = width;
moonx = x;
moony = y;
t_phase = -1;
}
function calcmoonphase(day, month, year) {
var r = (year % 100);
r = (r % 19);
if (r>9) {
r = r - 19;
}
r = ((r * 11) % 30) + month + day;
if (month<3) {
r = r + 2;
}
r = 1.0*r - 8.3 + 0.5;
r = (r.toNumber() % 30);
if (r < 0) {
r = r + 30;
}
return r;
}
function updateable_calcmoonphase(dc, dateinfo, hour) {
if (t_phase != dateinfo.day) {
t_phase = dateinfo.day;
c_phase = calcmoonphase(dateinfo.day, dateinfo.month, dateinfo.year);
if (hour > 12) { // change it at noon
c_phase = (c_phase + 1) % 30;
}
calc_drawmoon(c_phase);// updates c_moon_label and c_moon_y
}
drawmoon(dc, moonx, moony); // uses c_moon_y
return c_phase;
}
function drawmoon(dc, moonx, moony) {
dc.drawBitmap(moonx, moony, moon_bitmap);
var x, xby2;
dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_WHITE);
for (x=1; x<moon_width; x++) {
xby2 = x*2;
if (c_moon_y[xby2] >= 0) {
dc.drawLine(moonx+x, moony+c_moon_y[xby2], moonx+x, moony+c_moon_y[xby2+1]);
} else {
dc.drawLine(moonx+x, moony+1, moonx+x, moony-c_moon_y[xby2]);
dc.drawLine(moonx+x, moony-c_moon_y[xby2+1], moonx+x, moony+moon_width);
}
}
}
function calc_drawmoon(moonphase) {
var mw = moon_width; // image width
var c = mw/2; // image center
var intc = c.toNumber();
var r = (mw-2)*0.5-0.5; // radius depends on image
c_moon_label = "";
var step = 1;
var r1edge= -1;
var rSedge= 0;
var r1rest= -1;
var rSrest= 0;
var edgelight = false;
if (moonphase <= 8) {
c_moon_label = "wax.";
r1edge = intc; rSedge = step;
r1rest = intc; rSrest = -step;
edgelight = true;
if (moonphase == 8) {
r1edge = -1; rSedge = 0;
} else {
if (moonphase == 0) {
c_moon_label = "new";
}
}
} else {
if (moonphase <=16) {
c_moon_label = "wax.";
r1rest = -1; rSrest = 0;
r1edge = intc; rSedge = -step;
edgelight = false;
if (moonphase == 16) {
c_moon_label = "full";
r1edge = -1; rSedge = 0;
}
} else {
c_moon_label = "wan.";
if (moonphase <=23) {
r1rest = -1; rSrest = 0;
r1edge = intc; rSedge = step;
edgelight = false;
if (moonphase == 23) {
r1edge = -1; rSedge = 0;
r1rest = intc; rSrest = step;
}
} else {
r1edge = intc; rSedge = -step;
r1rest = intc; rSrest = step;
edgelight = true;
}
}
}
var a;
if (moonphase > 16) {
a = 1.0 - (moonphase - 16.0) / 7.0;
} else {
a = 1.0 - moonphase/8.0;
}
c_moon_y = new [mw*2+2];
var i;
for (i = 0; i<mw*2; i++) {
c_moon_y[i] = 0;
}
var x, xx, ra, sq, y1, y2, xby2;
for (x=r1rest; x<=mw && x>=1; x+=rSrest) {
//dc.drawLine(moonx+x, moony+1, moonx+x, moony+mw);
xby2 = 2*x;
c_moon_y[xby2] = 1;
c_moon_y[xby2+1] = mw;
}
for (x=r1edge; x<=mw && x>=1; x+=rSedge) {
xx = (x-c)/a;
ra = r*r - xx*xx;
xby2 = 2*x;
if (ra > 0) {
sq = Math.sqrt(ra);
y1 = c - sq + 0.5;
y1 = y1.toNumber();
y2 = c + sq + 0.5;
y2 = y2.toNumber();
if (edgelight) {
// dc.drawLine(moonx+x, moony+y1, moonx+x, moony+y2);
c_moon_y[xby2] = y1;
c_moon_y[xby2+1] = y2;
} else {
//dc.drawLine(moonx+x, moony+1, moonx+x, moony+y1);
//dc.drawLine(moonx+x, moony+y2, moonx+x, moony+mw);
c_moon_y[xby2] = -y1;
c_moon_y[xby2+1] = -y2;
}
} else {
if (!edgelight) {
// dc.drawLine(moonx+x, moony+1, moonx+x, moony+mw);
c_moon_y[xby2] = 1;
c_moon_y[xby2+1] = mw;
}
}
}
return c_moon_label;
}
}
And here's SunlightTerminator.mc:
using Toybox.Math as Math;
using Toybox.System as Sys;
using Toybox.WatchUi as Ui;
using Toybox.Graphics as Gfx;
// By using updateable_sunlight_terminator the terminator will be drawn, but it will
// only be recalculated every 300 seconds. The declination will be recalculated daily.
class SunlightTerminator {
var earthx, earthy, width, height;
var c_declination, t_declination; // day of month when last updated
var c_sunlight, t_sunlight; // time in seconds of next update.
var image;
function initialize(earth_bitmap, w, h, x, y) {
image = earth_bitmap;
t_declination = -1;
t_sunlight = -1;
earthx = x;
earthy = y;
width = w;
height = h;
}
function sun_declination(dateinfo) {
var jul = julian( dateinfo.year, dateinfo.month, dateinfo.day);
var radians = Math.PI/180.0;
var lambda = ecliptic_longitude(jul, radians);
var obliquity = 23.439 * radians - 0.0000004 * radians * jul;
var delta = Math.asin(Math.sin(obliquity) * Math.sin(lambda));
if (delta == 0) {
return 0.000001;
} else {
return delta;
}
}
function updateable_sun_declination(dateinfo) {
if (t_declination != dateinfo.day) {
c_declination = sun_declination(dateinfo);
t_declination = dateinfo.day;
}
return c_declination;
}
function ecliptic_longitude(jul, radians) {
var meanlongitude = getAngle(280.461 * radians + 0.9856474 * radians * jul);
var meananomaly = getAngle(357.528 * radians + .9856003 * radians * jul);
return getAngle(meanlongitude + 1.915 * radians * Math.sin(meananomaly)
+ 0.02 * radians * Math.sin(2.0 * meananomaly));
}
// returns a floating point angle in the range 0 .. 2*pi
function getAngle(x) {
var b = 0.5*x / Math.PI;
var a = 2.0*Math.PI * (b - b.toNumber());
if (a < 0) {
a = 2.0*pi + a;
}
return a;
}
// between 1901 to 2099
function julian( y, m, d) {
var a = (m + 9)/12.0;
var b = (y + a.toNumber())/4.0;
var c = 275*m/9.0;
var l = -7 * b.toNumber() + c.toNumber() + d;
l = l.toNumber() + y*367;
return l - 730531;
}
function updateable_sunlight_terminator(dc, time_sec, dateinfo, time) {
if (time_sec.value() > t_sunlight) {
t_sunlight = time_sec.value()+300; // update interval of 300 seconds
updateable_sun_declination(dateinfo);
calc_sunlight_terminator(dc, c_declination, time, earthx, earthy, width, height);
// updates c_sunlight array
}
// uses c_sunlight array
draw_sunlight_terminator(dc, c_declination, time, earthx, earthy, width, height);
}
function draw_sunlight_terminator(dc, declination, time, earthx, earthy, width, height){
dc.drawBitmap(earthx, earthy, image);
//hide some unnecessary part of the map:
dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_WHITE);
dc.drawLine(earthx,earthy,earthx+100,earthy);
dc.fillRectangle(earthx,earthy+47,earthx+100,earthy+50);
var x, x2;
if (declination < 0) {
for (x=1; x<=c_sunlight.size(); x+=1) {
x2 = earthx + x*2 - 1;
dc.drawLine(x2, earthy, x2, earthy+c_sunlight[x-1]);
}
} else {
for (x=1; x<=c_sunlight.size(); x+=1) {
x2 = earthx + x*2 - 1;
dc.drawLine(x2, earthy+c_sunlight[x-1], x2, earthy+height);
}
}
}
function calc_sunlight_terminator(dc, declination, time, earthx, earthy, width, height) {
var num_el = width / 2;
c_sunlight = new [num_el.toNumber()];
var hour = (((time.hour*3600-time.timeZoneOffset)%86400) - 43200 + time.min*60.0) / 3600.0;
//var lat1 = -1.5708; // -pi/2
//var lat2 = 1.5708; // pi/2
//var latrange = lat2 - lat1;
//lat1 = (-lat1+1.5708)/3.1416;
var latrange = 3.1416;
var lat1 = 1.0;
var x, y, longitude, latitude, x2;
x2 = 0;
if (declination < 0) {
for (x=2; x<=width; x+=2) {
longitude = (x-1.0)/width*6.2832-3.1416+hour/24*6.2832;
latitude = Math.atan(-Math.cos(longitude)/Math.tan(declination));
y = (-latitude + 1.5708) / latrange - (1-lat1);
y = y * height + 0.5;
y = y.toNumber();
//x2 = earthx + x - 1;
//dc.drawLine(x2, earthy, x2, earthy+y);
c_sunlight[x2] = y;
x2 += 1;
}
} else {
for (x=2; x<=width; x+=2) {
longitude = (x-1.0)/width*6.2832-3.1416+hour/24*6.2832;
latitude = Math.atan(-Math.cos(longitude)/Math.tan(declination));
y = (-latitude + 1.5708) / latrange - (1-lat1);
y = y * height + 0.5;
y = y.toNumber();
//x2 = earthx + x - 1;
//dc.drawLine(x2, earthy+y, x2, earthy+height);
c_sunlight[x2] = y;
x2 +=1;
}
}
}
}
Suscribirse a:
Entradas (Atom)