martes, 1 de marzo de 2016

Open source Connect IQ face for Garmin watches - Analog & Skies (lunar phase and sunlight terminator)

Download Analog & Bars [source code | prg for vivoactive | Garmin store]
Download Analog & Skies [source code | prg for vivoactive | Garmin store]

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;
            }
        }
    }
 
   

}














1 comentario:

  1. Nice post!
    Garmin is my best-chosen sports watch. This watch is amazing for many good reasons. I found many health-related good things on this watch. Click here to read Garmin Forerunner 735XT Review.\

    ResponderEliminar