<commit_before>/* jshint browser: true */
/* global console, Pebble */

// TODO use a real build process to share this with the config page
var VERSION = '0.0.2';

var SGV_FETCH_SECONDS = 4 * 60 * 60;
var INTERVAL_SIZE_SECONDS = 5 * 60;
var FETCH_EXTRA = 5;
var IOB_RECENCY_THRESHOLD_SECONDS = 10 * 60;
var REQUEST_TIMEOUT = 5000;
var NO_DELTA_VALUE = 65536;
var DEXCOM_ERROR_CODE_MAX = 12;

var CONFIG_URL = 'https://mddub.github.io/nightscout-graph-pebble/config/';
var LOCAL_STORAGE_KEY_CONFIG = 'config';

var MSG_TYPE_ERROR = 0;
var MSG_TYPE_DATA = 1;
var MSG_TYPE_PREFERENCES = 2;

var DEFAULT_CONFIG = {
  nightscout_url: '',
  mmol: false,
  topOfGraph: 250,
  topOfRange: 200,
  bottomOfRange: 70,
  bottomOfGraph: 40,
  hGridlines: 50,
  statusContent: 'pumpiob',
  statusUrl: '',
  timeAlign: 'center',
  batteryLoc: 'statusRight',
};

var ALIGN = {
  'left': 0,
  'center': 1,
  'right': 2,
};

var BATTERY_LOC = {
  'none': 0,
  'statusRight': 1,
  'timeTopLeft': 2,
  'timeTopRight': 3,
  'timeBottomLeft': 4,
  'timeBottomRight': 5,
};

var config;

function mergeConfig(config, defaults) {
  var out = {};
  Object.keys(defaults).forEach(function(key) {
    out[key] = defaults[key];
  });
  Object.keys(config).forEach(function(key) {
    out[key] = config[key];
  });
  return out;
}

function sgvDataError(e) {
  console.log(e);
  sendMessage({msgType: MSG_TYPE_ERROR});
}

// In PebbleKit JS, specifying a timeout works only for synchronous XHR,
// except on Android, where synchronous XHR doesn't work at all.
// https://forums.getpebble.com/discussion/13224/problem-with-xmlhttprequest-timeout
function getURL(url, callback) {
  var received = false;
  var timedOut = false;

  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.onreadystatechange = function () {
    if (timedOut) {
      return;
    }
    if (xhr.readyState === 4) {
      received = true;
      if (xhr.status === 200) {
        callback(null, xhr.responseText);
      } else {
        callback(new Error('Request failed, status ' + xhr.status + ': ' + url));
      }
    }
  };

  function onTimeout() {
    if (received) {
      return;
    }
    timedOut = true;
    xhr.abort();
    callback(new Error('Request timed out: ' + url));
  }

  // On iOS, PebbleKit JS will throw an error on send() for an invalid URL
  try {
    xhr.send();
    setTimeout(onTimeout, REQUEST_TIMEOUT);
  } catch (e) {
    callback(e);
  }
}

function getJSON(url, callback) {
  getURL(url, function(err, result) {
    if (err) {
      return callback(err);
    }
    try {
      callback(null, JSON.parse(result));
    } catch (e) {
      callback(e);
    }
  });
}

function getIOB(config, callback) {
  getJSON(config.nightscout_url + '/api/v1/entries.json?find[activeInsulin][$exists]=true&count=1', function(err, iobs) {
    if (err) {
      return callback(err);
    }
    if(iobs.length && Date.now() - iobs[0]['date'] <= IOB_RECENCY_THRESHOLD_SECONDS * 1000) {
      var recency = Math.floor((Date.now() - iobs[0]['date']) / (60 * 1000));
      var iob = iobs[0]['activeInsulin'].toFixed(1).toString() + ' u (' + recency + ')';
      callback(null, iob);
    } else {
      callback(null, '-');
    }
  });
}

function getCustomUrl(config, callback) {
  getURL(config.statusUrl, callback);
}

function getStatusText(config, callback) {
  var defaultFn = getIOB;
  var fn = {
    'pumpiob': getIOB,
    'customurl': getCustomUrl,
  }[config.statusContent];
  (fn || defaultFn)(config, callback);
}

function getSGVsDateDescending(config, callback) {
  var fetchStart = Date.now() - SGV_FETCH_SECONDS * 1000;
  var points = SGV_FETCH_SECONDS / INTERVAL_SIZE_SECONDS + FETCH_EXTRA;
  var url = config.nightscout_url + '/api/v1/entries/sgv.json?find[date][$gte]=' + fetchStart + '&count=' + points;
  getJSON(url, function(err, entries) {
    if (err) {
      return callback(err);
    }
    callback(null, entries.map(function(e) {
      e['date'] = e['date'] / 1000;
      return e;
    }));
  });
}

function graphArray(sgvs) {
  var endTime = sgvs[0]['date'];
  var noEntry = {
    'date': Infinity,
    'sgv': 0
  };
  var i;

  var graphed = [];
  var xs = [];
  for(i = 0; i <= SGV_FETCH_SECONDS; i += INTERVAL_SIZE_SECONDS) {
    graphed.push(noEntry);
    xs.push(endTime - i);
  }

  for(i = 0; i < sgvs.length; i++) {
    var min = Infinity;
    var xi;
    // Don't graph error codes
    if(sgvs[i]['sgv'] <= DEXCOM_ERROR_CODE_MAX) {
      continue;
    }
    // Find the x value closest to this sgv's date
    for(var j = 0; j < xs.length; j++) {
      if(Math.abs(sgvs[i]['date'] - xs[j]) < min) {
        min = Math.abs(sgvs[i]['date'] - xs[j]);
        xi = j;
      }
    }<commit_msg>Don't crash on raw-only sgv entries (entries missing sgv)<commit_after>/* jshint browser: true */
/* global console, Pebble */

// TODO use a real build process to share this with the config page
var VERSION = '0.0.2';

var SGV_FETCH_SECONDS = 4 * 60 * 60;
var INTERVAL_SIZE_SECONDS = 5 * 60;
var FETCH_EXTRA = 5;
var IOB_RECENCY_THRESHOLD_SECONDS = 10 * 60;
var REQUEST_TIMEOUT = 5000;
var NO_DELTA_VALUE = 65536;
var DEXCOM_ERROR_CODE_MAX = 12;

var CONFIG_URL = 'https://mddub.github.io/nightscout-graph-pebble/config/';
var LOCAL_STORAGE_KEY_CONFIG = 'config';

var MSG_TYPE_ERROR = 0;
var MSG_TYPE_DATA = 1;
var MSG_TYPE_PREFERENCES = 2;

var DEFAULT_CONFIG = {
  nightscout_url: '',
  mmol: false,
  topOfGraph: 250,
  topOfRange: 200,
  bottomOfRange: 70,
  bottomOfGraph: 40,
  hGridlines: 50,
  statusContent: 'pumpiob',
  statusUrl: '',
  timeAlign: 'center',
  batteryLoc: 'statusRight',
};

var ALIGN = {
  'left': 0,
  'center': 1,
  'right': 2,
};

var BATTERY_LOC = {
  'none': 0,
  'statusRight': 1,
  'timeTopLeft': 2,
  'timeTopRight': 3,
  'timeBottomLeft': 4,
  'timeBottomRight': 5,
};

var config;

function mergeConfig(config, defaults) {
  var out = {};
  Object.keys(defaults).forEach(function(key) {
    out[key] = defaults[key];
  });
  Object.keys(config).forEach(function(key) {
    out[key] = config[key];
  });
  return out;
}

function sgvDataError(e) {
  console.log(e);
  sendMessage({msgType: MSG_TYPE_ERROR});
}

// In PebbleKit JS, specifying a timeout works only for synchronous XHR,
// except on Android, where synchronous XHR doesn't work at all.
// https://forums.getpebble.com/discussion/13224/problem-with-xmlhttprequest-timeout
function getURL(url, callback) {
  var received = false;
  var timedOut = false;

  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.onreadystatechange = function () {
    if (timedOut) {
      return;
    }
    if (xhr.readyState === 4) {
      received = true;
      if (xhr.status === 200) {
        callback(null, xhr.responseText);
      } else {
        callback(new Error('Request failed, status ' + xhr.status + ': ' + url));
      }
    }
  };

  function onTimeout() {
    if (received) {
      return;
    }
    timedOut = true;
    xhr.abort();
    callback(new Error('Request timed out: ' + url));
  }

  // On iOS, PebbleKit JS will throw an error on send() for an invalid URL
  try {
    xhr.send();
    setTimeout(onTimeout, REQUEST_TIMEOUT);
  } catch (e) {
    callback(e);
  }
}

function getJSON(url, callback) {
  getURL(url, function(err, result) {
    if (err) {
      return callback(err);
    }
    try {
      callback(null, JSON.parse(result));
    } catch (e) {
      callback(e);
    }
  });
}

function getIOB(config, callback) {
  getJSON(config.nightscout_url + '/api/v1/entries.json?find[activeInsulin][$exists]=true&count=1', function(err, iobs) {
    if (err) {
      return callback(err);
    }
    if(iobs.length && Date.now() - iobs[0]['date'] <= IOB_RECENCY_THRESHOLD_SECONDS * 1000) {
      var recency = Math.floor((Date.now() - iobs[0]['date']) / (60 * 1000));
      var iob = iobs[0]['activeInsulin'].toFixed(1).toString() + ' u (' + recency + ')';
      callback(null, iob);
    } else {
      callback(null, '-');
    }
  });
}

function getCustomUrl(config, callback) {
  getURL(config.statusUrl, callback);
}

function getStatusText(config, callback) {
  var defaultFn = getIOB;
  var fn = {
    'pumpiob': getIOB,
    'customurl': getCustomUrl,
  }[config.statusContent];
  (fn || defaultFn)(config, callback);
}

function getSGVsDateDescending(config, callback) {
  var fetchStart = Date.now() - SGV_FETCH_SECONDS * 1000;
  var points = SGV_FETCH_SECONDS / INTERVAL_SIZE_SECONDS + FETCH_EXTRA;
  var url = config.nightscout_url + '/api/v1/entries/sgv.json?find[date][$gte]=' + fetchStart + '&count=' + points;
  getJSON(url, function(err, entries) {
    if (err) {
      return callback(err);
    }
    callback(null, entries.map(function(e) {
      e['date'] = e['date'] / 1000;
      return e;
    }));
  });
}

function graphArray(sgvs) {
  var endTime = sgvs[0]['date'];
  var noEntry = {
    'date': Infinity,
    'sgv': 0
  };
  var i;

  var graphed = [];
  var xs = [];
  for(i = 0; i <= SGV_FETCH_SECONDS; i += INTERVAL_SIZE_SECONDS) {
    graphed.push(noEntry);
    xs.push(endTime - i);
  }

  for(i = 0; i < sgvs.length; i++) {
    var min = Infinity;
    var xi;
    // Don't graph missing sgvs or error codes
    if(sgvs[i]['sgv'] === undefined || sgvs[i]['sgv'] <= c.DEXCOM_ERROR_CODE_MAX) {
      continue;
    }
    // Find the x value closest to this sgv's date
    for(var j = 0; j < xs.length; j++) {
      if(Math.abs(sgvs[i]['date'] - xs[j]) < min) {
        min = Math.abs(sgvs[i]['date'] - xs[j]);
        xi = j;
      }
    }
{'fraction_non_alphanumeric': 0.15809443507588533, 'fraction_numerical': 0.01897133220910624, 'mean_word_length': 3.154553415061296, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 3, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1147680', 'n_tokens_mistral': 3773, 'n_tokens_neox': 3541, 'n_words': 791}