/*
Speed and Altitude [speedalt2]
Mike Bennett mike[at]kereru.com
1.10 : add inverted colours
1.14 : Add VMG screen
1.34 : Add bluetooth data stream for Droidscript
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
1.50 : Add cfg.wptSfx one char suffix to append to waypoints.json filename. Protects speedalt2 waypoints from other apps that use the same file name for waypoints.
*/
var v = '1.50';
var vDroid = '1.50';    // Required DroidScript program version

/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
var KalmanFilter = (function () {
  'use strict';

  function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
      throw new TypeError("Cannot call a class as a function");
    }
  }

  function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }

  function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
  }

  /**
  * KalmanFilter
  * @class
  * @author Wouter Bulten
  * @see {@link http://github.com/wouterbulten/kalmanjs}
  * @version Version: 1.0.0-beta
  * @copyright Copyright 2015-2018 Wouter Bulten
  * @license MIT License
  * @preserve
  */
  var KalmanFilter =
  /*#__PURE__*/
  function () {
    /**
    * Create 1-dimensional kalman filter
    * @param  {Number} options.R Process noise
    * @param  {Number} options.Q Measurement noise
    * @param  {Number} options.A State vector
    * @param  {Number} options.B Control vector
    * @param  {Number} options.C Measurement vector
    * @return {KalmanFilter}
    */
    function KalmanFilter() {
      var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
          _ref$R = _ref.R,
          R = _ref$R === void 0 ? 1 : _ref$R,
          _ref$Q = _ref.Q,
          Q = _ref$Q === void 0 ? 1 : _ref$Q,
          _ref$A = _ref.A,
          A = _ref$A === void 0 ? 1 : _ref$A,
          _ref$B = _ref.B,
          B = _ref$B === void 0 ? 0 : _ref$B,
          _ref$C = _ref.C,
          C = _ref$C === void 0 ? 1 : _ref$C;

      _classCallCheck(this, KalmanFilter);

      this.R = R; // noise power desirable

      this.Q = Q; // noise power estimated

      this.A = A;
      this.C = C;
      this.B = B;
      this.cov = NaN;
      this.x = NaN; // estimated signal without noise
    }
    /**
    * Filter a new value
    * @param  {Number} z Measurement
    * @param  {Number} u Control
    * @return {Number}
    */


    _createClass(KalmanFilter, [{
      key: "filter",
      value: function filter(z) {
        var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

        if (isNaN(this.x)) {
          this.x = 1 / this.C * z;
          this.cov = 1 / this.C * this.Q * (1 / this.C);
        } else {
          // Compute prediction
          var predX = this.predict(u);
          var predCov = this.uncertainty(); // Kalman gain

          var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction

          this.x = predX + K * (z - this.C * predX);
          this.cov = predCov - K * this.C * predCov;
        }

        return this.x;
      }
      /**
      * Predict next value
      * @param  {Number} [u] Control
      * @return {Number}
      */

    }, {
      key: "predict",
      value: function predict() {
        var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
        return this.A * this.x + this.B * u;
      }
      /**
      * Return uncertainty of filter
      * @return {Number}
      */

    }, {
      key: "uncertainty",
      value: function uncertainty() {
        return this.A * this.cov * this.A + this.R;
      }
      /**
      * Return the last filtered measurement
      * @return {Number}
      */

    }, {
      key: "lastMeasurement",
      value: function lastMeasurement() {
        return this.x;
      }
      /**
      * Set measurement noise Q
      * @param {Number} noise
      */

    }, {
      key: "setMeasurementNoise",
      value: function setMeasurementNoise(noise) {
        this.Q = noise;
      }
      /**
      * Set the process noise R
      * @param {Number} noise
      */

    }, {
      key: "setProcessNoise",
      value: function setProcessNoise(noise) {
        this.R = noise;
      }
    }]);

    return KalmanFilter;
  }();

  return KalmanFilter;

}());


var buf = Graphics.createArrayBuffer(240,160,2,{msb:true});


let LED = // LED as minimal and only definition (as instance / singleton)
{ isOn: false // status on / off, not needed if you don't need to ask for it
, set: function(v) { // turn on w/ no arg or truey, else off
   this.isOn = v===undefined||!!v;
   g.setColor(this.isOn?1:0,0,0).fillCircle(120,10,10); }
, reset: function() { this.set(false); } // turn off
, write: function(v) { this.set(v); }  // turn on w/ no arg or truey, else off
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
}, LED1 = LED; // LED1 as 'synonym' for LED


var lf = {fix:0,satellites:0};
var showMax = 0;        // 1 = display the max values. 0 = display the cur fix
var pwrSav = 1;         // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
var canDraw = 1;
var tmrLP;            // Timer for delay in switching to low power after screen turns off

var maxSpd = 0;
var maxAlt = 0;
var maxN = 0;    // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.

var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0;  // 1 = running in emulator. Supplies test values;
var bt = 0;         // 0 = bluetooth data feed off. 1 = on
var btLast = 0;     // time of last bt transmit

var wp = {};        // Waypoint to use for distance from cur position.

function nxtWp(){
  cfg.wp++;
  loadWp();
}

function loadWp() {
  var w = require("waypoints").load(cfg.wptSfx);
  if (cfg.wp>=w.length) cfg.wp=0;
  if (cfg.wp<0) cfg.wp = w.length-1;
  savSettings();
  wp = w[cfg.wp];
}

function radians(a) {
  return a*Math.PI/180;
}

function degrees(a) {
  var d = a*180/Math.PI;
  return (d+360)%360;
}

function bearing(a,b){
  var delta = radians(b.lon-a.lon);
  var alat = radians(a.lat);
  var blat = radians(b.lat);
  var y = Math.sin(delta) * Math.cos(blat);
  var x = Math.cos(alat)*Math.sin(blat) -
        Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
  return Math.round(degrees(Math.atan2(y, x)));
}

function distance(a,b){
  var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
  var y = radians(b.lat-a.lat);

  // Distance in metres
  var d = Math.sqrt(x*x + y*y) * 6371000;
  return d;
}

function drawScrn(dat) {

  if (!canDraw) return;

  buf.clear();
  buf.setBgColor(0);

  var n;
  n = dat.val.toString();

  var s=50;    // Font size
  var l=n.length;

  if ( l <= 7 ) s=55;
  if ( l <= 6 ) s=60;
  if ( l <= 5 ) s=80;
  if ( l <= 4 ) s=100;
  if ( l <= 3 ) s=120;

  buf.setFontAlign(0,0); //Centre
  buf.setColor(1);
  buf.setFontVector(s);
  buf.drawString(n,126,52);


  // Primary Units
  buf.setFontAlign(-1,1); //left, bottom
  buf.setColor(2);
  buf.setFontVector(35);
  buf.drawString(dat.unit,5,164);

  drawMax(dat.max); // MAX display indicator
  drawWP(dat.wp);  // Waypoint name
  drawSats(dat.sats);

  g.reset();
  g.drawImage(img,0,40);

  LED1.write(!pwrSav);

}

function drawPosn(dat) {
  if (!canDraw) return;
  buf.clear();
  buf.setBgColor(0);

  var x, y;
  x=210;
  y=0;
  buf.setFontAlign(1,-1);
  buf.setFontVector(60);
  buf.setColor(1);

  buf.drawString(dat.lat,x,y);
  buf.drawString(dat.lon,x,y+70);

  x = 240;
  buf.setColor(2);
  buf.setFontVector(40);
  buf.drawString(dat.ns,x,y);
  buf.drawString(dat.ew,x,y+70);


  drawSats(dat.sats);

  g.reset();
  g.drawImage(img,0,40);

  LED1.write(!pwrSav);

}

function drawClock() {
  if (!canDraw) return;

  buf.clear();
  buf.setBgColor(0);

  var x, y;
  x=185;
  y=0;
  buf.setFontAlign(1,-1);
  buf.setFontVector(94);
  const time = require("locale").time(new Date(),1);

  buf.setColor(1);

  buf.drawString(time.substring(0,2),x,y);
  buf.drawString(time.substring(3,5),x,y+80);

  g.reset();
  g.drawImage(img,0,40);

  LED1.write(!pwrSav);
}

function drawWP(wp) {
  buf.setColor(3);
  buf.setFontAlign(0,1); //left, bottom
  buf.setFontVector(40);
  buf.drawString(wp,120,132);
}

function drawSats(sats) {
  buf.setColor(3);
  buf.setFont("6x8", 2);
  buf.setFontAlign(1,1); //right, bottom
  buf.drawString(sats,240,160);
}

function drawMax(max) {
  buf.setFontVector(30);
  buf.setColor(2);
  buf.setFontAlign(0,1); //centre, bottom
  buf.drawString(max,120,164);
}

function onGPS(fix) {
if ( emulator ) {
    fix.fix = 1;
    fix.speed = 10 + (Math.random()*5);
    fix.alt = 354 + (Math.random()*50);
    fix.lat = -38.92;
    fix.lon = 175.7613350;
    fix.course = 245;
    fix.satellites = 12;
    fix.time = new Date();
    fix.smoothed = 0;
  }

  //var m;

  var sp = '---';
  var al = sp;
  var di = sp;
  var brg = ''; // bearing
  var crs = ''; // course
  var age = sp;
  var lat = '---.--';
  var ns = '';
  var ew = '';
  var lon = '---.--';
  var sats = sp;
  var vmg = sp;


  // Waypoint name
  var wpName = wp.name;
  if ( wpName == undefined || wpName == 'NONE' ) wpName = '';
  wpName = wpName.substring(0,8);

  if (fix.fix) lf = fix;

  if (lf.fix) {

    // Smooth data
    if ( lf.smoothed !== 1 ) {
      if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
      if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
      lf.smoothed = 1;
      if ( maxN <= 15 ) maxN++;
    }

    // Bearing to waypoint
    brg = bearing(lf,wp);

    // Current course
    crs = lf.course;

    // Relative angle to wp
    var a = Math.max(crs,brg) - Math.min(crs,brg);
    if ( a >= 180 ) a = 360 -a;

    // Speed
    sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units

    // vmg
    if ( a >= 90 ) vmg = sp * Math.cos(radians(180-a)) * -1;  // moving away from WP
    else vmg = sp * Math.cos(radians(a));  // towards wp

    if ( sp < 10 ) sp = sp.toFixed(1);
    else sp = Math.round(sp);
    if (isNaN(sp)) sp = '---';
    if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;

    if ( Math.abs(vmg) >= 0.05 && Math.abs(vmg) < 10 ) vmg = vmg.toFixed(1);
    else vmg = Math.round(vmg);
    if (isNaN(vmg)) vmg = '---';

    // Altitude
    al = lf.alt;
    al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
    if (parseFloat(al) > parseFloat(maxAlt) && maxN > 15 ) maxAlt = al;
    if (isNaN(al)) al = '---';

    // Distance to waypoint
    di = distance(lf,wp);
    di = (di/parseFloat(cfg.dist)).toFixed(2);
    if ( di >= 100 ) di = parseFloat(di).toFixed(1);
    if ( di >= 1000 ) di = parseFloat(di).toFixed(0);
    if (isNaN(di)) di = '------';

    // Age of last fix (secs)
    age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));

    // Lat / Lon
    ns = 'N';
    if ( lf.lat < 0 ) ns = 'S';
    lat = Math.abs(lf.lat.toFixed(2));

    ew = 'E';
    if ( lf.lon < 0 ) ew = 'W';
    lon = Math.abs(lf.lon.toFixed(2));

    // Sats
    if ( age > 10 ) {
      sats = 'Age:'+Math.round(age);
      if ( age > 90 ) sats = 'Age:>90';
    }
    else sats = 'Sats:'+lf.satellites;

  }

  // Bluetooth send data
  btSend({
    id:'speedalt2',
    v:v,
    vd:vDroid,
    m:cfg.modeA,
    spd_unit:cfg.spd_unit,
    alt_unit:cfg.alt_unit,
    dist_unit:cfg.dist_unit,
    wp:wpName,
    sp:sp,
    al:al,
    di:di,
    sats:sats,
    vmg:vmg,
    lat:lat,
    lon:lon,
    ns:ns,
    ew:ew
  });

  if ( cfg.modeA == 0 )  {
    // Speed
    if ( showMax ) {
      drawScrn({
        val:maxSpd,
        unit:cfg.spd_unit,
        sats:sats,
        age:age,
        max:'MAX',
        wp:''
      }); // Speed maximums
    }
    else {
      drawScrn({
        val:sp,
        unit:cfg.spd_unit,
        sats:sats,
        age:age,
        max:'SPD',
        wp:''
      });
    }
  }

  if ( cfg.modeA == 1 ) {
    // Alt
    if ( showMax ) {
      drawScrn({
        val:maxAlt,
        unit:cfg.alt_unit,
        sats:sats,
        age:age,
        max:'MAX',
        wp:''
      }); // Alt maximums
    }
    else {
      drawScrn({
        val:al,
        unit:cfg.alt_unit,
        sats:sats,
        age:age,
        max:'ALT',
        wp:''
      });
    }
  }

  if ( cfg.modeA == 2 ) {
    // Dist
      drawScrn({
        val:di,
        unit:cfg.dist_unit,
        sats:sats,
        age:age,
        max:'DST',
        wp:wpName
      });
  }

  if ( cfg.modeA == 3 ) {
    // VMG
      drawScrn({
        val:vmg,
        unit:cfg.spd_unit,
        sats:sats,
        age:age,
        max:'VMG',
        wp:wpName
      });
  }

  if ( cfg.modeA == 4 ) {
    // Position
    drawPosn({
        sats:sats,
        age:age,
        lat:lat,
        lon:lon,
        ns:ns,
        ew:ew
      });
  }

  if ( cfg.modeA == 5 )  {
    // Large clock
    drawClock();
  }

}

function prevScrn() {
    cfg.modeA = cfg.modeA-1;
    if ( cfg.modeA < 0 ) cfg.modeA = 5;
    savSettings();
    onGPS(lf);
}

function nextScrn() {
    cfg.modeA = cfg.modeA+1;
    if ( cfg.modeA > 5 ) cfg.modeA = 0;
    savSettings();
    onGPS(lf);
}

// Next function on a screen
function nextFunc(dur) {
    if ( cfg.modeA == 0 || cfg.modeA == 1 ) {
      // Spd+Alt mode - Switch between fix and MAX
      if ( dur < 2 ) showMax = !showMax;   // Short press toggle fix/max display
      else { maxSpd = 0; maxAlt = 0; }  // Long press resets max values.
    }
    else  if ( cfg.modeA == 2 || cfg.modeA == 3) nxtWp();  // Dist or VMG mode - Select next waypoint
    onGPS(lf);
}


function updateClock() {
  if (!canDraw) return;
  if ( cfg.modeA != 5 )  return;
  drawClock();
  if ( emulator ) {maxSpd++;maxAlt++;}
}

function startDraw(){
  canDraw=true;
  g.clear();
  Bangle.drawWidgets();
  setLpMode('SuperE'); // off
  onGPS(lf);  // draw app screen
}

function stopDraw() {
  canDraw=false;
  if ( bt ) return;    // If bt screen mirror to Droidscript in use then keep GPS in SuperE mode to keep screen updates going.
  if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000);   //Drop to low power in 10 secs. Keep lp mode off until we have a  first fix.
}

function savSettings() {
  require("Storage").write('speedalt2.json',cfg);
}

function setLpMode(m) {
  if (tmrLP) {clearInterval(tmrLP);tmrLP = false;} // Stop any scheduled drop to low power
  if ( !gpssetup ) return;
  gpssetup.setPowerMode({power_mode:m});
}

// == Droidscript bluetooth data

function btOn(b) {
  bt = b;                                   // Turn data transmit on/off
}

function btSend(dat) {
  if ( ! bt ) return;                       // bt transmit off
  var dur = getTime() - btLast;
  if ( dur < 1.0 ) return;                  // Don't need to transmit more than every 1.0 secs.
  btLast = getTime();
  Bluetooth.println(JSON.stringify(dat));         // transmit the data
}

// == Events

function setButtons(){

  // BTN1 - Max speed/alt or next waypoint
  setWatch(function(e) {
    var dur = e.time - e.lastTime;
    nextFunc(dur);
  }, BTN1, { edge:"falling",repeat:true});

  // Power saving on/off
  setWatch(function(e){
    pwrSav=!pwrSav;
    if ( pwrSav ) {
      var s = require('Storage').readJSON('setting.json',1)||{};
      var t = s.timeout||10;
      Bangle.setLCDTimeout(t);
    }
    else {
      Bangle.setLCDTimeout(0);
//      Bangle.setLCDPower(1);
    }
      LED1.write(!pwrSav);
  }, BTN2, {repeat:true,edge:"falling"});

  // BTN3 - next screen
  setWatch(function(e){
    nextScrn();
  }, BTN3, {repeat:true,edge:"falling"});
}

Bangle.on('lcdPower',function(on) {
  if (!SCREENACCESS.withApp) return;
  if (on) startDraw();
  else stopDraw();
});

Bangle.on('swipe',function(dir) {
  if ( ! cfg.touch ) return;
  if(dir == 1) prevScrn();
  else nextScrn();
});

Bangle.on('touch', function(button){
  if ( ! cfg.touch ) return;
  nextFunc(0);  // Same function as short BTN1
});

// == Main Prog

// Read settings.
let cfg = require('Storage').readJSON('speedalt2.json',1)||{};

cfg.spd = cfg.spd||1;  // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = cfg.spd_unit||'kph';  // Displayed speed unit
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
cfg.alt_unit = cfg.alt_unit||'feet';  // Displayed altitude units
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
cfg.dist_unit = cfg.dist_unit||'km';  // Displayed altitude units
cfg.colour = cfg.colour||0;          // Colour scheme.
cfg.wp = cfg.wp||0;        // Last selected waypoint for dist
cfg.modeA = cfg.modeA||0;    // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock
cfg.primSpd = cfg.primSpd||0;    // 1 = Spd in primary, 0 = Spd in secondary

cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt;
cfg.touch = cfg.touch==undefined?true:cfg.touch;
cfg.wptSfx = cfg.wptSfx==undefined?'':cfg.wptSfx;

if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });

loadWp();

/*
Colour Pallet Idx
0 : Background (black)
1 : Speed/Alt
2 : Units
3 : Sats
*/
var img = {
  width:buf.getWidth(),
  height:buf.getHeight(),
  bpp:2,
  buffer:buf.buffer,
  palette:new Uint16Array([0,0x4FE0,0xEFE0,0x07DB])
};

if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]);
if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xF800,0xFAE0,0xF813]);
if ( cfg.colour == 3 ) img.palette = new Uint16Array([0xFFFF,0x007F,0x0054,0x0054]);

var SCREENACCESS = {
      withApp:true,
      request:function(){this.withApp=false;stopDraw();},
      release:function(){this.withApp=true;startDraw();}
};

var gpssetup;
try {
  gpssetup = require("gpssetup");
} catch(e) {
  gpssetup = false;
}

// All set up. Lets go.
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
onGPS(lf);
Bangle.setGPSPower(1);

if ( gpssetup ) {
  gpssetup.setPowerMode({power_mode:"SuperE"}).then(function() { Bangle.setGPSPower(1); });
}
else {
  Bangle.setGPSPower(1);
}

Bangle.on('GPS', onGPS);

setButtons();
setInterval(updateClock, 10000);
