<commit_before>var types = require("../lib/HAP-NodeJS/accessories/types.js");
var request = require('request');
var net = require('net')
/* TODO:
 * - send keepalive
 * - auto reconnect on disconnect
 * - wait for success after command and then send the next
 * - support more devices
 */

var DEBUG = true;

PilightDeviceTypes = {
  RAW: 0,
  SWITCH: 1,
  DIMMER: 2,
  WEATHER: 3,
  RELAY: 4,
  SCREEN: 5,
  CONTACT: 6,
  PENDINGSW: 7,
  DATETIME: 8,
  XBMC: 9,
  LIRC: 10,
  WEBCAM: 11
}

function PilightPlatform(log, config){
  this.name = config['name']
  this.host = config['host']
  this.port = config['port']
  this.protocol = config['protocol']
  this.http_port = config['http-port']

  this.log = log;
}

function PilightClient(log, platform) {
    var that= this;

    this.log = log;
    this.platform = platform;
    this.registry = [];

    this.connect();
    this.startHeartBeat();
}

PilightClient.prototype = {
  send: function(cmd) {
    var msg = JSON.stringify(cmd);
    this.log("[PilightClient.send] "+msg);
    this.socket.write(msg);
  },

  register: function(idx, accessory) {
    this.registry[idx] = accessory;
  },

  updateDevices: function(devices, values) {
    for(var device of devices) {
      var dev = this.registry[device]
      if(dev === undefined || dev.length === 0) {
        continue
      }
      this.log("[PilightClient.updateDevices] External update on device "
               + device);
      dev.updateCharacteristics(values);
    }
  },

  startHeartBeat: function() {
    var that = this;
    var cb = function() { that.socket.write("HEART\n", "utf-8"); };
    this.heartBeatInterval = setInterval(cb, 1000);
  },

  onBeat: function() {
    if(DEBUG) { this.log("BEAT");}

    if(this.heartBeatTimeout) {
      clearTimeout(this.heartBeatTimeout);
    }

    var that = this;
    var reconnectDelay = 10000;
    var reconnect = function() {
      that.log('[PilightClient.reconnect] Trying reconnect');
      try{
        that.socket.end();
        that.socket.destroy();
      }
      finally {
        that.connect();
      }
    }
    this.heartBeatTimeout = setTimeout(reconnect, reconnectDelay);
  },

  connect: function() {
    var that = this;
    var bootstrap = function() {
      that.log('connected to server!');
      var cmd = {
        "action": "identify",
        "options": {
          "config": 1
        }
      };
      that.send(cmd);
    }

    this.socket = net.connect({host:this.platform.host, port:this.platform.port}, bootstrap);

    this.socket.on('data', function(data) {
      data = data.toString();
      for(msg of data.split('\n')) {
        if(msg.length === 0) { continue; }

        if(msg === "BEAT") {
          that.onBeat();
          continue;
        }

        try {
          data = JSON.parse(data);
        } catch(e) {
          if (e instanceof SyntaxError) { that.log("SyntaxError in " + msg); }
          else { that.log("Error " + JSON.stringify(e)); }
          continue;
        }
        if(data['origin'] !== 'update') {
          continue;
        }
        that.updateDevices(data['devices'], data['values']);
      }
    });

    this.socket.on('end', function() {
      clearTimeout(that.heartBeatTimeout);
      clearTimeout(that.heartBeatInterval);
      that.log('disconnected from server');
    });
  }
}

PilightPlatform.prototype = {
  accessories: function(callback) {
    // XXX request config over socket crashes pilight
    var url = this.protocol + '://' + this.host + ':'
                + this.http_port +'/config';

    this.log("[PilightPlatform.accessories] Fetching pilight devices.");
    this.log("[PilightPlatform.accessories] Fetching from: "+url);

    var collected = [];
    var that = this;
    var client = new PilightClient(this.log, this);

    request.get({url: url, json:true}, function(err, response, json) {
      var accessory = null;

      if (!err && response.statusCode == 200) {

        for (var device_name in json['gui']) {

          var gui = json['gui'][device_name]
          var device = json['devices'][device_name]
          var accessory;

          for (var proto of device['protocol']) {

            that.log("[PilightPlatform.accessories] " + gui['name'] + " : " + proto);

            switch(proto) {
              case "elro_800_switch":
                accessory = PilightSwitch;
                break;
              case 'openweathermap':
                accessory = PilightThermostat;
                break;
              default: // arping, sunriseset, datetime, generic_label
                that.log("Unsupported pilight device (" + gui['name']
                         + ") with protocol " + device['protocol']);
                continue;
            }
            that.log("Adding pilight device (" + gui['name'] + ") with protocol "
                     + device['protocol']);
            collected.push(
                    new accessory(that.log, device_name, gui['name'], proto, device, client));
            break;  // we found a supported protocol, so we are done with this device
          }
        }
        callback(collected);
      } else {
        that.log("There was a problem requesting the configuration from pilight.");
      }
    });
  }
}

function PilightSwitch(log, idx, name, protocol, info, client) {
  // device info
  log("Name: "+idx);
  this.idx = idx;
  this.name = name;
  this.log = log;
  this.client = client;
  this.registry = [];
  this.info = info;
  this.protocol = protocol;
}

PilightSwitch.prototype = {

  registerForUpdates: function(characteristic) {
    this.log("[PilightSwitch.registerForUpdates] Registering device "+this.idx
             + " for updates on characteristic '"+characteristic.manfDescription+"'");

    this.registry.push(characteristic);
    this.client.register(this.idx, this);
  },

  updateCharacteristics: function(values) {
    var newValue = (values['state'] === 'on')*1;
    for(var c of this.registry) {
      if(c.value === newValue) {
        this.log("[PilightSwitch.updateCharacteristics] No state change "
                 + "detected on characteristic, skiping event");
        continue;
      }
      this.log("[PilightSwitch.updateCharacteristics] Updating device "
               +this.name + " to value "+newValue+" on characteristic '"
               +JSON.stringify(c.manfDescription)+"'");
      c.updateValue(newValue);
    }
  },

  setPowerState: function(powerOn) {
    var that = this;
    this.log("[PilightSwitch.setPowerState] "+powerOn);
    this.client.send({
          "action": "control",
          "code": {
            "device": that.idx,
            "state": (powerOn)?"on":"off"
          }});
  },

  getServices: function() {
    var that = this;
    return [{
      sType: types.ACCESSORY_INFORMATION_STYPE,
      characteristics: [{
        cType: types.NAME_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: that.name,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Name of the accessory",
        designedMaxLength: 255
      },{
        cType: types.MANUFACTURER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "pilight",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Manufacturer",
        designedMaxLength: 255
      },{
        cType: types.MODEL_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "Rev-1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Model",
        designedMaxLength: 255
      },{
        cType: types.SERIAL_NUMBER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "BIVAB-PO-1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "SN",
        designedMaxLength: 255
      },{
        cType: types.IDENTIFY_CTYPE,
        onUpdate: null,
        perms: ["pw"],
        format: "bool",
        initialValue: false,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Identify Accessory",
        designedMaxLength: 1
      }]
    },{
      sType: types.OUTLET_STYPE,
      characteristics: [{
        cType: types.NAME_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: that.name + " outlet service",
        supportEvents: true,
        supportBonjour: false,
        manfDescription: "Name of service",
        designedMaxLength: 255
      },{
        cType: types.POWER_STATE_CTYPE,
        onUpdate: function(value) { that.setPowerState(value)},
        onRegister: function(characteristic) { that.registerForUpdates(characteristic) },
        perms: ["pw","pr","ev"],
        format: "bool",
        initialValue: (this.state==='on')?1:0,
        supportEvents: true,
        supportBonjour: false,
        manfDescription: "Change the power state of the outlet",
        designedMaxLength: 1
      },{
        cType: types.OUTLET_IN_USE_CTYPE,
        onUpdate: function(value) { that.setPowerState(value)},
        perms: ["pr","ev"],
        format: "bool",
        initialValue: true,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Turn On the Light",
        designedMaxLength: 1
      }]
    }];
  }
};

// XXX refactory and merge with Switch
function PilightThermostat(log, idx, name, protocol, info, client) {
  // device info
  log("Name: "+idx);
  this.idx = idx;
  this.name = name;
  this.log = log;
  this.client = client;
  this.registry = [];
  this.info = info;
  this.protocol = protocol;
}

// XXX merge with Switch prototype and extract common prototype
PilightThermostat.prototype = {

  registerForUpdates: function(characteristic) {
    this.log("[PilightThermostat.registerForUpdates] Registering device "+this.idx
             + " for updates on characteristic '"+characteristic.manfDescription+"'");

    this.registry.push(characteristic);
    this.client.register(this.idx, this);
  },

  updateCharacteristics: function(values) {
    this.log(values);
    var newValue;
    for(var c of this.registry) {
      switch(c.type) {
        case types.CURRENT_TEMPERATURE_CTYPE:
          newValue = values['temperature'];
          break;
        case types.CURRENT_RELATIVE_HUMIDITY_CTYPE:
          newValue = values['humidity'];
          break;
      }
      if(newValue === undefined || c.value === newValue) {
        this.log("[PilightThermostat.updateCharacteristics] No state change "
                 + "detected on characteristic, skiping event");
        continue;
      }
      this.log("[PilightThermostat.updateCharacteristics] Updating device "
               +this.name + " to value "+newValue+" on characteristic '"
               +JSON.stringify(c.manfDescription)+"'");
    }
    c.updateValue(newValue);
  },

  getServices: function() {
    var that = this;
    return [{
      sType: types.ACCESSORY_INFORMATION_STYPE,
      characteristics: [{
        cType: types.NAME_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: that.name + "Thermostat 1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.MANUFACTURER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "Oltica",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.MODEL_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "Rev-1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.SERIAL_NUMBER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "A1S2NASF88EW",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.IDENTIFY_CTYPE,
        onUpdate: null,
        perms: ["pw"],
        format: "bool",
        initialValue: false,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Identify Accessory",
        designedMaxLength: 1
      }]
      },{
      sType: types.THERMOSTAT_STYPE,
      characteristics: [{
          cType: types.NAME_CTYPE,
          onUpdate: null,
          perms: ["pr"],
          format: "string",
          initialValue: that.name + "Thermostat Control",
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Bla",
          designedMaxLength: 255
        },{
          cType: types.CURRENTHEATINGCOOLING_CTYPE,
          perms: ["pr","ev"],
          format: "int",
          initialValue: 0,
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Current Mode",
          designedMaxLength: 1,
          designedMinValue: 0,
          designedMaxValue: 2,
          designedMinStep: 1,
        },{
          cType: types.TARGETHEATINGCOOLING_CTYPE,
          perms: ["pr","ev"],
          format: "int",
          initialValue: 0,
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Target Mode",
          designedMinValue: 0,
          designedMaxValue: 3,
          designedMinStep: 1,
        },{
          cType: types.CURRENT_TEMPERATURE_CTYPE,
          onRegister: function(characteristic) { that.registerForUpdates(characteristic) },
          onUpdate: null,
          perms: ["pr","ev"],
          format: "int",
          initialValue: parseFloat(that.info['temperature']),
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Current Temperature",
          unit: "celsius"
        },{
          cType: types.TARGET_TEMPERATURE_CTYPE,
          onUpdate: null,
          perms: ["pr","ev"],
          format: "int",
          initialValue: parseFloat(that.info['temperature']),
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Target Temperature",
          designedMinValue: 16,
          designedMaxValue: 38,
          designedMinStep: 1,
          unit: "celsius"
        },{
          cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE,
          onRegister: function(characteristic) { that.registerForUpdates(characteristic) },
          onUpdate: null,
          perms: ["pr","ev"],
          format: "float",
          initialValue: parseFloat(that.info['humidity']),
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Current relative humidity",
          unit: "percentage",
          designedMinValue: 0,
          designedMaxValue: 100,
          designedMinStep: 1
        },{
          cType: types.TEMPERATURE_UNITS_CTYPE,
          perms: ["pr","ev"],
          format: "int",
          initialValue: 0,
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Unit",
        }]
      }];
  }
};
module.exports.accessory = [PilightSwitch, PilightThermostat];
module.exports.platform = PilightPlatform;
<commit_msg>tweak delays<commit_after>var types = require("../lib/HAP-NodeJS/accessories/types.js");
var request = require('request');
var net = require('net')
/* TODO:
 * - send keepalive
 * - auto reconnect on disconnect
 * - wait for success after command and then send the next
 * - support more devices
 */

var DEBUG = true;

PilightDeviceTypes = {
  RAW: 0,
  SWITCH: 1,
  DIMMER: 2,
  WEATHER: 3,
  RELAY: 4,
  SCREEN: 5,
  CONTACT: 6,
  PENDINGSW: 7,
  DATETIME: 8,
  XBMC: 9,
  LIRC: 10,
  WEBCAM: 11
}

function PilightPlatform(log, config){
  this.name = config['name']
  this.host = config['host']
  this.port = config['port']
  this.protocol = config['protocol']
  this.http_port = config['http-port']

  this.log = log;
}

function PilightClient(log, platform) {
    var that = this;

    this.log = log;
    this.platform = platform;
    this.registry = [];

    this.connect();
    this.startHeartBeat();
}

PilightClient.prototype = {
  send: function(cmd) {
    var msg = JSON.stringify(cmd);
    this.log("[PilightClient.send] "+msg);
    this.socket.write(msg);
  },

  register: function(idx, accessory) {
    this.registry[idx] = accessory;
  },

  updateDevices: function(devices, values) {
    for(var device of devices) {
      var dev = this.registry[device]
      if(dev === undefined || dev.length === 0) {
        continue
      }
      this.log("[PilightClient.updateDevices] External update on device "
               + device);
      dev.updateCharacteristics(values);
    }
  },

  startHeartBeat: function() {
    var that = this;
    var cb = function() { that.socket.write("HEART\n", "utf-8"); };
    this.heartBeatInterval = setInterval(cb, 20000);
  },

  onBeat: function() {
    if(DEBUG) { this.log("BEAT");}

    if(this.heartBeatTimeout) {
      clearTimeout(this.heartBeatTimeout);
    }

    var that = this;
    var reconnectDelay = 30000;
    var reconnect = function() {
      that.log('[PilightClient.reconnect] Trying reconnect');
      try{
        that.socket.end();
        that.socket.destroy();
      }
      finally {
        that.connect();
      }
    }
    this.heartBeatTimeout = setTimeout(reconnect, reconnectDelay);
  },

  connect: function() {
    var that = this;
    var bootstrap = function() {
      that.log('connected to server!');
      var cmd = {
        "action": "identify",
        "options": {
          "config": 1
        }
      };
      that.send(cmd);
    }

    this.socket = net.connect({host:this.platform.host, port:this.platform.port}, bootstrap);

    this.socket.on('data', function(data) {
      data = data.toString();
      for(msg of data.split('\n')) {
        if(msg.length === 0) { continue; }

        if(msg === "BEAT") {
          that.onBeat();
          continue;
        }

        try {
          data = JSON.parse(data);
        } catch(e) {
          if (e instanceof SyntaxError) { that.log("SyntaxError in " + msg); }
          else { that.log("Error " + JSON.stringify(e)); }
          continue;
        }
        if(data['origin'] !== 'update') {
          continue;
        }
        that.updateDevices(data['devices'], data['values']);
      }
    });

    this.socket.on('end', function() {
      clearTimeout(that.heartBeatTimeout);
      clearTimeout(that.heartBeatInterval);
      that.log('disconnected from server');
    });
  }
}

PilightPlatform.prototype = {
  accessories: function(callback) {
    // XXX request config over socket crashes pilight
    var url = this.protocol + '://' + this.host + ':'
                + this.http_port +'/config';

    this.log("[PilightPlatform.accessories] Fetching pilight devices.");
    this.log("[PilightPlatform.accessories] Fetching from: "+url);

    var collected = [];
    var that = this;
    var client = new PilightClient(this.log, this);

    request.get({url: url, json:true}, function(err, response, json) {
      var accessory = null;

      if (!err && response.statusCode == 200) {

        for (var device_name in json['gui']) {

          var gui = json['gui'][device_name]
          var device = json['devices'][device_name]
          var accessory;

          for (var proto of device['protocol']) {

            that.log("[PilightPlatform.accessories] " + gui['name'] + " : " + proto);

            switch(proto) {
              case "elro_800_switch":
                accessory = PilightSwitch;
                break;
              case 'openweathermap':
                accessory = PilightThermostat;
                break;
              default: // arping, sunriseset, datetime, generic_label
                that.log("Unsupported pilight device (" + gui['name']
                         + ") with protocol " + device['protocol']);
                continue;
            }
            that.log("Adding pilight device (" + gui['name'] + ") with protocol "
                     + device['protocol']);
            collected.push(
                    new accessory(that.log, device_name, gui['name'], proto, device, client));
            break;  // we found a supported protocol, so we are done with this device
          }
        }
        callback(collected);
      } else {
        that.log("There was a problem requesting the configuration from pilight.");
      }
    });
  }
}

function PilightSwitch(log, idx, name, protocol, info, client) {
  // device info
  log("Name: "+idx);
  this.idx = idx;
  this.name = name;
  this.log = log;
  this.client = client;
  this.registry = [];
  this.info = info;
  this.protocol = protocol;
}

PilightSwitch.prototype = {

  registerForUpdates: function(characteristic) {
    this.log("[PilightSwitch.registerForUpdates] Registering device "+this.idx
             + " for updates on characteristic '"+characteristic.manfDescription+"'");

    this.registry.push(characteristic);
    this.client.register(this.idx, this);
  },

  updateCharacteristics: function(values) {
    var newValue = (values['state'] === 'on')*1;
    for(var c of this.registry) {
      if(c.value === newValue) {
        this.log("[PilightSwitch.updateCharacteristics] No state change "
                 + "detected on characteristic, skiping event");
        continue;
      }
      this.log("[PilightSwitch.updateCharacteristics] Updating device "
               +this.name + " to value "+newValue+" on characteristic '"
               +JSON.stringify(c.manfDescription)+"'");
      c.updateValue(newValue);
    }
  },

  setPowerState: function(powerOn) {
    var that = this;
    this.log("[PilightSwitch.setPowerState] "+powerOn);
    this.client.send({
          "action": "control",
          "code": {
            "device": that.idx,
            "state": (powerOn)?"on":"off"
          }});
  },

  getServices: function() {
    var that = this;
    return [{
      sType: types.ACCESSORY_INFORMATION_STYPE,
      characteristics: [{
        cType: types.NAME_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: that.name,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Name of the accessory",
        designedMaxLength: 255
      },{
        cType: types.MANUFACTURER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "pilight",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Manufacturer",
        designedMaxLength: 255
      },{
        cType: types.MODEL_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "Rev-1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Model",
        designedMaxLength: 255
      },{
        cType: types.SERIAL_NUMBER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "BIVAB-PO-1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "SN",
        designedMaxLength: 255
      },{
        cType: types.IDENTIFY_CTYPE,
        onUpdate: null,
        perms: ["pw"],
        format: "bool",
        initialValue: false,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Identify Accessory",
        designedMaxLength: 1
      }]
    },{
      sType: types.OUTLET_STYPE,
      characteristics: [{
        cType: types.NAME_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: that.name + " outlet service",
        supportEvents: true,
        supportBonjour: false,
        manfDescription: "Name of service",
        designedMaxLength: 255
      },{
        cType: types.POWER_STATE_CTYPE,
        onUpdate: function(value) { that.setPowerState(value)},
        onRegister: function(characteristic) { that.registerForUpdates(characteristic) },
        perms: ["pw","pr","ev"],
        format: "bool",
        initialValue: (this.state==='on')?1:0,
        supportEvents: true,
        supportBonjour: false,
        manfDescription: "Change the power state of the outlet",
        designedMaxLength: 1
      },{
        cType: types.OUTLET_IN_USE_CTYPE,
        onUpdate: function(value) { that.setPowerState(value)},
        perms: ["pr","ev"],
        format: "bool",
        initialValue: true,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Turn On the Light",
        designedMaxLength: 1
      }]
    }];
  }
};

// XXX refactory and merge with Switch
function PilightThermostat(log, idx, name, protocol, info, client) {
  // device info
  log("Name: "+idx);
  this.idx = idx;
  this.name = name;
  this.log = log;
  this.client = client;
  this.registry = [];
  this.info = info;
  this.protocol = protocol;
}

// XXX merge with Switch prototype and extract common prototype
PilightThermostat.prototype = {

  registerForUpdates: function(characteristic) {
    this.log("[PilightThermostat.registerForUpdates] Registering device "+this.idx
             + " for updates on characteristic '"+characteristic.manfDescription+"'");

    this.registry.push(characteristic);
    this.client.register(this.idx, this);
  },

  updateCharacteristics: function(values) {
    this.log(values);
    var newValue;
    for(var c of this.registry) {
      switch(c.type) {
        case types.CURRENT_TEMPERATURE_CTYPE:
          newValue = values['temperature'];
          break;
        case types.CURRENT_RELATIVE_HUMIDITY_CTYPE:
          newValue = values['humidity'];
          break;
      }
      if(newValue === undefined || c.value === newValue) {
        this.log("[PilightThermostat.updateCharacteristics] No state change "
                 + "detected on characteristic, skiping event");
        continue;
      }
      this.log("[PilightThermostat.updateCharacteristics] Updating device "
               +this.name + " to value "+newValue+" on characteristic '"
               +JSON.stringify(c.manfDescription)+"'");
    }
    c.updateValue(newValue);
  },

  getServices: function() {
    var that = this;
    return [{
      sType: types.ACCESSORY_INFORMATION_STYPE,
      characteristics: [{
        cType: types.NAME_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: that.name + "Thermostat 1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.MANUFACTURER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "Oltica",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.MODEL_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "Rev-1",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.SERIAL_NUMBER_CTYPE,
        onUpdate: null,
        perms: ["pr"],
        format: "string",
        initialValue: "A1S2NASF88EW",
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Bla",
        designedMaxLength: 255
        },{
        cType: types.IDENTIFY_CTYPE,
        onUpdate: null,
        perms: ["pw"],
        format: "bool",
        initialValue: false,
        supportEvents: false,
        supportBonjour: false,
        manfDescription: "Identify Accessory",
        designedMaxLength: 1
      }]
      },{
      sType: types.THERMOSTAT_STYPE,
      characteristics: [{
          cType: types.NAME_CTYPE,
          onUpdate: null,
          perms: ["pr"],
          format: "string",
          initialValue: that.name + "Thermostat Control",
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Bla",
          designedMaxLength: 255
        },{
          cType: types.CURRENTHEATINGCOOLING_CTYPE,
          perms: ["pr","ev"],
          format: "int",
          initialValue: 0,
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Current Mode",
          designedMaxLength: 1,
          designedMinValue: 0,
          designedMaxValue: 2,
          designedMinStep: 1,
        },{
          cType: types.TARGETHEATINGCOOLING_CTYPE,
          perms: ["pr","ev"],
          format: "int",
          initialValue: 0,
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Target Mode",
          designedMinValue: 0,
          designedMaxValue: 3,
          designedMinStep: 1,
        },{
          cType: types.CURRENT_TEMPERATURE_CTYPE,
          onRegister: function(characteristic) { that.registerForUpdates(characteristic) },
          onUpdate: null,
          perms: ["pr","ev"],
          format: "int",
          initialValue: parseFloat(that.info['temperature']),
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Current Temperature",
          unit: "celsius"
        },{
          cType: types.TARGET_TEMPERATURE_CTYPE,
          onUpdate: null,
          perms: ["pr","ev"],
          format: "int",
          initialValue: parseFloat(that.info['temperature']),
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Target Temperature",
          designedMinValue: 16,
          designedMaxValue: 38,
          designedMinStep: 1,
          unit: "celsius"
        },{
          cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE,
          onRegister: function(characteristic) { that.registerForUpdates(characteristic) },
          onUpdate: null,
          perms: ["pr","ev"],
          format: "float",
          initialValue: parseFloat(that.info['humidity']),
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Current relative humidity",
          unit: "percentage",
          designedMinValue: 0,
          designedMaxValue: 100,
          designedMinStep: 1
        },{
          cType: types.TEMPERATURE_UNITS_CTYPE,
          perms: ["pr","ev"],
          format: "int",
          initialValue: 0,
          supportEvents: false,
          supportBonjour: false,
          manfDescription: "Unit",
        }]
      }];
  }
};
module.exports.accessory = [PilightSwitch, PilightThermostat];
module.exports.platform = PilightPlatform;
<|endoftext|>


<commit_before><commit_msg>added a little more flexibility to object comparison, to support arrays with different order of elements<commit_after><|endoftext|>


<commit_before>// ==UserScript==
// @name         memo
// @namespace    https://github.com/mihaiBura/memo
// @version      0.1
// @description  No more forgetting important things.
// @author       You
// @match        *://*/*
// @grant        mbura
// ==/UserScript==

(function(window, document) {
    'use strict';

    var k = getLocationHash(window.location),
        textbox = createTextbox();

    /*
    * Function that outputs a hash of the current location
    * Based on hostname, pathname, and search.
    * Trying to avoid different ports, protocols, hashes
    * , and other href alterations
    *
    * See more:
    * https://developer.mozilla.org/en-US/docs/Web/API/Location
    * http://bl.ocks.org/abernier/3070589
    */
    function getLocationHash(location) {
        return location.hostname + location.pathname + location.search;
    }

    function getData(key){
        return window.localStorage.getItem(key);
    }

    function setData(key, data) {
        window.localStorage.setItem(key, data);
    }

    function renderData(data) {
        textbox.value = data;
    }

    function createTextbox() {
        var wrapper = document.createElement('div'),
            textbox = document.createElement('textarea');

        wrapper.appendChild(textbox);

        document.body.insertBefore(wrapper, document.body.firstChild);

        return textbox;
    }

    function listenForChanges() {
        var text = textbox.value;
        setData(k, text);
    }

    /*
    * Kickstart
    */
    renderData(getData(k));
    textbox.onkeyup = listenForChanges;
})(window, document);<commit_msg>Fix author<commit_after>// ==UserScript==
// @name         memo
// @namespace    https://github.com/mihaiBura/memo
// @version      0.1
// @description  No more forgetting important things.
// @author       mbura
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function(window, document) {
    'use strict';

    var k = getLocationHash(window.location),
        textbox = createTextbox();

    /*
    * Function that outputs a hash of the current location
    * Based on hostname, pathname, and search.
    * Trying to avoid different ports, protocols, hashes
    * , and other href alterations
    *
    * See more:
    * https://developer.mozilla.org/en-US/docs/Web/API/Location
    * http://bl.ocks.org/abernier/3070589
    */
    function getLocationHash(location) {
        return location.hostname + location.pathname + location.search;
    }

    function getData(key){
        return window.localStorage.getItem(key);
    }

    function setData(key, data) {
        window.localStorage.setItem(key, data);
    }

    function renderData(data) {
        textbox.value = data;
    }

    function createTextbox() {
        var wrapper = document.createElement('div'),
            textbox = document.createElement('textarea');

        wrapper.appendChild(textbox);

        document.body.insertBefore(wrapper, document.body.firstChild);

        return textbox;
    }

    function listenForChanges() {
        var text = textbox.value;
        setData(k, text);
    }

    /*
    * Kickstart
    */
    renderData(getData(k));
    textbox.onkeyup = listenForChanges;
})(window, document);<|endoftext|>


<commit_before>var https = require('https');
var qs = require('querystring');
var AWS = require('aws-sdk');
var s3 = new AWS.S3();

var config = {
  untappdClientId: '',
  untappdClientSecret: '',
  bucketName: ''
};

var areas = {
  'tampere':  {name: 'tampere',  lat: 61.4985, lng: 23.7717, radius: 1.06},
  'helsinki': {name: 'helsinki', lat: 60.1671, lng: 24.9409, radius: 1.49},
};
var area = areas['tampere'];

// Get latest beer checkins in a geographical area and store them to S3 as json.
exports.handler = function(event, context) {

  var httpQueryParams = {
    client_id: config.untappdClientId,
    client_secret: config.untappdClientSecret,
    lat: area.lat,
    lng: area.lng,
    radius: area.radius
  };

  var httpOptions = {
    host: 'api.untappd.com',
    path: '/v4/thepub/local' + '?' + qs.stringify(httpQueryParams)
  };

  https.get(httpOptions, function(res) {
    console.log('Remote server rate limit remaining: ', res.headers['x-ratelimit-remaining']);
    if (res.statusCode != 200) {
      console.log('Received non-ok status code from remote: ', res.statusCode);
      context.fail('Request to remote failed');
    }

    var s3params = {
      'Bucket': config.bucketName,
      'Key': 'data/thepub-' + area.name + '.json',
      'ContentLength': res.headers['content-length'],
      'ContentType': 'application/json',
      'Body': res
    };

    s3.upload(s3params, function(err, data) {
      if (err) {
        console.log('Error while uploading to S3: ', err, err.stack);
        context.fail('Failed to upload data to S3');
      }
      else {
        context.succeed('Successfully updated data for area ' + area.name);
      }
    });

  }).on('error', function(e) {
    console.log(e.message);
    context.fail('Failed to get response from remote');
  });

};
<commit_msg>Unify code style<commit_after>"use strict";

var https = require('https');
var qs = require('querystring');
var AWS = require('aws-sdk');
var s3 = new AWS.S3();

var config = {
  remoteClientId: '',
  remoteClientSecret: '',
  remoteHost: '',
  remotePath: '',
  bucketName: ''
};

var areas = {
  tampere:  {name: 'tampere',  lat: 61.4985, lng: 23.7717, radius: 1.06},
  helsinki: {name: 'helsinki', lat: 60.1671, lng: 24.9409, radius: 1.49},
};
var area = areas.tampere;

// Get latest checkins in a geographical area and store them to S3 as json.
exports.handler = function(event, context) {

  var httpQueryParams = {
    client_id: config.remoteClientId,
    client_secret: config.remoteClientSecret,
    lat: area.lat,
    lng: area.lng,
    radius: area.radius
  };

  var httpOptions = {
    host: config.remoteHost,
    path: config.remotePath + '?' + qs.stringify(httpQueryParams)
  };

  https.get(httpOptions, function(res) {
    console.log('Remote server rate limit remaining: ', res.headers['x-ratelimit-remaining']);
    if (res.statusCode != 200) {
      console.log('Received non-ok status code from remote: ', res.statusCode);
      context.fail('Request to remote failed');
    }

    var s3params = {
      'Bucket': config.bucketName,
      'Key': 'data/thepub-' + area.name + '.json',
      'ContentLength': res.headers['content-length'],
      'ContentType': 'application/json',
      'Body': res
    };

    s3.upload(s3params, function(err, data) {
      if (err) {
        console.log('Error while uploading to S3: ', err, err.stack);
        context.fail('Failed to upload data to S3');
      }
      else {
        context.succeed('Successfully updated data for area ' + area.name);
      }
    });

  }).on('error', function(e) {
    console.log(e.message);
    context.fail('Failed to get response from remote');
  });

};
<|endoftext|>


<commit_before>module.exports = {
  "env": {
    "browser": true,
    "es6": true,
  },
  "plugins": [
    "react",
  ],
  "globals": {
    "graphql": false,
  },
  "parserOptions": {
    "sourceType": "module",
    "ecmaFeatures": {
      "ecmaVersion": 2018,
      "jsx": true,
    },
  }
}
<commit_msg>fix(gatsby-starter-blog): ESLint 'ecmaVersion' in 'parserOptions' (#11258)<commit_after>module.exports = {
  "env": {
    "browser": true,
    "es6": true,
  },
  "plugins": [
    "react",
  ],
  "globals": {
    "graphql": false,
  },
  "parserOptions": {
    "sourceType": "module",
    "ecmaVersion": 2018,
    "ecmaFeatures": {
      "jsx": true,
    },
  }
}
<|endoftext|>


<commit_before>var locationHelper = (function () {
	/**
	 * CustomEvent polyfill
	 * @see https://developer.mozilla.org/ru/docs/Web/API/CustomEvent/CustomEvent
	 */
	var CustomEvent = window.CustomEvent;
	if (typeof CustomEvent !== 'function') {
		CustomEvent = function (event, params) {
			params = params || { bubbles: false, cancelable: false, detail: undefined };
			var evt = document.createEvent('CustomEvent');
			evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
			return evt;
		}
		CustomEvent.prototype = window.Event.prototype;
	}

	function LocationHelper() {
		var prevChange = null;

		this.trigger = function (type, detail) {
			document.dispatchEvent(new CustomEvent(type, {
				detail: detail,
				bubbles: true,
				cancelable: false,
			}));

			if (type == 'location-change') {
				// trigger relative sub-events:
				if (!prevChange || prevChange.parts.path != detail.parts.path) {
					this.trigger('location-change-path', {
						href: detail.href,
						parts: detail.parts,
						prevPath: prevChange ? prevChange.parts.path : null,
						path: detail.parts.path,
					});
				}
				if (!prevChange || prevChange.parts.search != detail.parts.search) {
					this.trigger('location-change-search', {
						href: detail.href,
						parts: detail.parts,
						prevSearch: prevChange ? prevChange.parts.search : null,
						search: detail.parts.search,
					});
				}
				if (!prevChange || prevChange.parts.hash != detail.parts.hash) {
					this.trigger('location-change-hash', {
						href: detail.href,
						parts: detail.parts,
						prevHash: prevChange ? prevChange.parts.hash : null,
						hash: detail.parts.hash,
					});
				}

				prevChange = JSON.parse(JSON.stringify({
					href: detail.href,
					parts: detail.parts,
				}));
			}
		};
	}

	LocationHelper.prototype.parseParams = function (s) {
		var res = {};
		s = s.split('&');
		var param;
		for (var i = 0; i < s.length; i++) {
			if (!s[i])
				continue;
			param = s[i].split('=', 2);
			LocationHelper.setValueByPath(res, param[0], param.length > 1 ? param[1] : true);
		}
		return res;
	};

	LocationHelper.setValueByPath = function (o, path, value) {
		if (!(path instanceof Array)) {
			path = path.replace(/^([^\[]+)/, '[$1]');
			path = path.slice(1, -1).split('][');
		}
		if (typeof value == 'string')
			value = decodeURIComponent(value);

		var name = path.pop();
		var res = o;
		var dir = false;
		while (path.length > 0) {
			dir = decodeURIComponent(path.shift());

			if (dir === '') {
				if (o[dir] && !(o[dir] instanceof Array))
					o[dir] = [];
			}
			else if (typeof o[dir] != 'object' || !o[dir]) {
				o[dir] = {};
			}

			if (path.length) {
				o = o[dir];
			}
		}

		if (name === '') {
			if (dir !== false && !(o[dir] instanceof Array))
				o[dir] = [];
			if (dir !== false) {
				var v = o[dir];
				delete o[dir];
				o[dir] = v;
				o = o[dir];
			}
			o.push(value);
		}
		else {
			if (dir !== false)
				o = o[dir];
			delete o[name];
			o[name] = value;
		}

		return o;
	};

	var parseURLRegExp = /^(?:(https?|file|[a-z]+):\/\/(?:([^\/:]+)(?::(\d+))?|))?(\/[^?#]*)?(\/?[^#]*)?(#\S*)?/;

	LocationHelper.prototype.parseURL = function (url) {
		var res = parseURLRegExp.exec(url);
		if (!res)
			return false;
		res = {
			schema: res[1],
			protocol: res[1] && res[1] + ':' || res[1],
			host: res[2],
			port: res[3],
			path: res[4],
			search: res[5],
			hash: res[6],
		};

		return res;
	};

	LocationHelper.prototype.buildURL = function (parts, baseParts) {
		if (!parts.protocol || parts.schema)
			parts.protocol = parts.schema + ':';
		else if (parts.protocol || !parts.schema)
			parts.schema = parts.protocol.slice(0, -1);

		if (baseParts) {
			if (!parts.protocol || !parts.host || !parts.port) {
				parts.protocol = baseParts.protocol;
				parts.schema = baseParts.schema;
				parts.host = baseParts.host;
				parts.port = baseParts.port;
			}
			if (!parts.path) {
				parts.path = baseParts.path;
			}
			if (parts.hash) {
				var newHash = parts.hash.split('!', 2);
				if (newHash.length < 2)
					newHash[1] = '';
				newHash[0] = newHash[0].length > 1 ? newHash[0] : (baseParts.hash && baseParts.hash.split('!')[0] || '');
				if (typeof newHash[1] == 'undefined') {
					newHash[1] = baseParts.hash && baseParts.hash.split('!')[1] || '';
				}
				else if (newHash[1][0] == '&') {
					newHash[1] = newHash[1].substr(1).split('&');
					var currentHashParams = this.getHashParams();
					for (var i = 0; i < newHash[1].length; i++) {
						var s = newHash[1][i].split('=', 2);
						LocationHelper.setValueByPath(currentHashParams, s[0], s[1]);
					}
					newHash[1] = this.buildParams(currentHashParams);
				}
				if (!newHash[1])
					newHash = [ newHash[0] ];
				if (newHash[1] && !newHash[0])
					newHash[0] = '#';
				parts.hash = newHash[0] == '#' && !newHash[1] ? '' : newHash.join('!');
			}
		}

		var s = [
			parts.path || '',
			parts.search || '',
			parts.hash || '',
		];

		if (parts.protocol == 'file:') {
			s.unshift(parts.protocol + '//');
		}
		else if (parts.host && parts.protocol) {
			if (parts.port)
				s.unshift(':' + parts.port);
			s.unshift(parts.host);
			s.unshift(parts.protocol + '//');
		}

		return s.join('');
	};

	LocationHelper.prototype.buildParams = function (o, parent) {
		if (typeof o != 'object')
			return parent + '=' + encodeURIComponent(o);

		var s = [];
		if (o instanceof Array) {
			for (var i = 0; i < o.length; i++) {
				var v = o[i];
				var path = parent ? parent + '[]' : '[]';
				s.push(this.buildParams(v, path));
			}
		}
		else {
			for (var k in o) {
				var v = o[k];
				var path = parent ? parent + '[' + encodeURIComponent(k) + ']' : encodeURIComponent(k);
				s.push(this.buildParams(v, path));
			}
		}
		return s.join('&');
	};

	LocationHelper.prototype.getHref = function () {
		return document.location.href;
	};

	LocationHelper.prototype.getHash = function () {
		return document.location.hash;
	};

	LocationHelper.prototype.getSearch = function () {
		return document.location.search;
	};

	LocationHelper.prototype.getPath = function () {
		return document.location.pathname;
	};

	LocationHelper.prototype.getHashParams = function () {
		var s = this.getHash();
		return this.parseParams(s.split('!', 2)[1] || '');
	};

	LocationHelper.prototype.getSearchParams = function () {
		var s = this.getSearch();
		return this.parseParams(s.slice(1));
	};

	LocationHelper.prototype.setLocation = function (hrefOrParts, replace, originalEvent) {
		var parts = typeof hrefOrParts == 'object' ? hrefOrParts : this.parseURL(hrefOrParts);
		var curParts = this.parseURL(this.getHref());
		var href = this.buildURL(parts, curParts);

		history[replace ? 'replaceState' : 'pushState']({
			href: href,
			parts: parts,
		}, '', href);

		this.trigger('location-change', {
			href: href,
			parts: parts,
			originalEvent: originalEvent,
		});

		return this;
	};

	LocationHelper.prototype.reload = function (originalEvent) {
		var href = this.getHref();
		var parts = this.parseURL(href);

		history['replaceState']({
			href: href,
			parts: parts,
		}, '', href);

		this.trigger('location-change', {
			href: href,
			parts: parts,
			originalEvent: originalEvent,
		});

		return this;
	};

	var bindParams = false;

	LocationHelper.prototype.bind = function (_bindParams) {
		_bindParams = _bindParams || true;
		if (!history || !history.pushState)
			return this;

		if (bindParams) {
			bindParams = _bindParams;
			return this;
		}
		bindParams = _bindParams;

		this.setLocation(document.location.href, true);
		var _this = this;

		window.addEventListener('popstate', function (event) {
			if (!event.state || !event.state.href || !event.state.parts)
				return;
			_this.trigger('location-change', {
				href: event.state.href,
				parts: event.state.parts,
				originalEvent: event,
			});
		}, false);

		document.body.addEventListener('click', function (event) {
			var a = event.target;
			while (a && a.nodeName != 'A')
				a = a.parentElement;
			if (a && a.target == '_null') {
				event.preventDefault();
				return false;
			}
			if (!a || !a.href || (a.target || '_self') != '_self')
				return;

			var parts = _this.parseURL(a.href);
			if (!parts || !/^https?:/.test(parts.protocol))
				return;

			var curParts = _this.parseURL(_this.getHref());
			if (parts.host != curParts.host || parts.protocol != curParts.protocol || parts.port != curParts.port)
				return;

			if (bindParams !== true && bindParams.path && curParts.path != parts.path)
				return;
			if (bindParams !== true && bindParams.search && curParts.search != parts.search)
				return;
			if (bindParams !== true && bindParams.hash && curParts.hash != parts.hash)
				return;

			_this.setLocation(parts, false, event);

			event.preventDefault();
		}, false);

		return this;
	}

	return new LocationHelper();
})();
<commit_msg>LocationHelper: detect array-like parameters<commit_after>var locationHelper = (function () {
	/**
	 * CustomEvent polyfill
	 * @see https://developer.mozilla.org/ru/docs/Web/API/CustomEvent/CustomEvent
	 */
	var CustomEvent = window.CustomEvent;
	if (typeof CustomEvent !== 'function') {
		CustomEvent = function (event, params) {
			params = params || { bubbles: false, cancelable: false, detail: undefined };
			var evt = document.createEvent('CustomEvent');
			evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
			return evt;
		}
		CustomEvent.prototype = window.Event.prototype;
	}

	function LocationHelper() {
		var prevChange = null;

		this.trigger = function (type, detail) {
			document.dispatchEvent(new CustomEvent(type, {
				detail: detail,
				bubbles: true,
				cancelable: false,
			}));

			if (type == 'location-change') {
				// trigger relative sub-events:
				if (!prevChange || prevChange.parts.path != detail.parts.path) {
					this.trigger('location-change-path', {
						href: detail.href,
						parts: detail.parts,
						prevPath: prevChange ? prevChange.parts.path : null,
						path: detail.parts.path,
					});
				}
				if (!prevChange || prevChange.parts.search != detail.parts.search) {
					this.trigger('location-change-search', {
						href: detail.href,
						parts: detail.parts,
						prevSearch: prevChange ? prevChange.parts.search : null,
						search: detail.parts.search,
					});
				}
				if (!prevChange || prevChange.parts.hash != detail.parts.hash) {
					this.trigger('location-change-hash', {
						href: detail.href,
						parts: detail.parts,
						prevHash: prevChange ? prevChange.parts.hash : null,
						hash: detail.parts.hash,
					});
				}

				prevChange = JSON.parse(JSON.stringify({
					href: detail.href,
					parts: detail.parts,
				}));
			}
		};
	}

	LocationHelper.prototype.parseParams = function (s) {
		var res = {};
		s = s.split('&');
		var param;
		for (var i = 0; i < s.length; i++) {
			if (!s[i])
				continue;
			param = s[i].split('=', 2);
			LocationHelper.setValueByPath(res, param[0], param.length > 1 ? param[1] : true);
		}
		LocationHelper.walkObjectTree(res, function (value, key, parent) {
			if (typeof value != 'object')
				return value;
			var arr = LocationHelper.isArray(value);
			return arr || value;
		});
		return res;
	};

	LocationHelper.setValueByPath = function (o, path, value) {
		if (!(path instanceof Array)) {
			path = path.replace(/^([^\[]+)/, '[$1]');
			path = path.slice(1, -1).split('][');
		}
		if (typeof value == 'string')
			value = decodeURIComponent(value);

		var name = path.pop();
		var res = o;
		var dir = false;
		while (path.length > 0) {
			dir = decodeURIComponent(path.shift());

			if (dir === '') {
				if (o[dir] && !(o[dir] instanceof Array))
					o[dir] = [];
			}
			else if (typeof o[dir] != 'object' || !o[dir]) {
				o[dir] = {};
			}

			if (path.length) {
				o = o[dir];
			}
		}

		if (name === '') {
			if (dir !== false && !(o[dir] instanceof Array))
				o[dir] = [];
			if (dir !== false) {
				var v = o[dir];
				delete o[dir];
				o[dir] = v;
				o = o[dir];
			}
			o.push(value);
		}
		else {
			if (dir !== false)
				o = o[dir];
			delete o[name];
			o[name] = value;
		}

		return o;
	};

	LocationHelper.isArray = function (o) {
		if (o instanceof Array)
			return true;
		if (typeof o != 'object' || !o)
			return false;
		var re = /^\d+$/;
		var arr = [];
		for (var k in o) {
			if (k == 'length')
				continue;
			if (!re.test(k))
				return false;
			arr[k] = o[k];
		}
		return arr;
	};

	LocationHelper.walkObjectTree = function (o, cb) {
		if (typeof o != 'object' || !o)
			return o;
		for (var k in o) {
			o[k] = cb(o[k], k, o);
			if (typeof o == 'object' && o) {
				this.walkObjectTree(o[k], cb);
			}
		}
		return o;
	};

	var parseURLRegExp = /^(?:(https?|file|[a-z]+):\/\/(?:([^\/:]+)(?::(\d+))?|))?(\/[^?#]*)?(\/?[^#]*)?(#\S*)?/;

	LocationHelper.prototype.parseURL = function (url) {
		var res = parseURLRegExp.exec(url);
		if (!res)
			return false;
		res = {
			schema: res[1],
			protocol: res[1] && res[1] + ':' || res[1],
			host: res[2],
			port: res[3],
			path: res[4],
			search: res[5],
			hash: res[6],
		};

		return res;
	};

	LocationHelper.prototype.buildURL = function (parts, baseParts) {
		if (!parts.protocol || parts.schema)
			parts.protocol = parts.schema + ':';
		else if (parts.protocol || !parts.schema)
			parts.schema = parts.protocol.slice(0, -1);

		if (baseParts) {
			if (!parts.protocol || !parts.host || !parts.port) {
				parts.protocol = baseParts.protocol;
				parts.schema = baseParts.schema;
				parts.host = baseParts.host;
				parts.port = baseParts.port;
			}
			if (!parts.path) {
				parts.path = baseParts.path;
			}
			if (parts.hash) {
				var newHash = parts.hash.split('!', 2);
				if (newHash.length < 2)
					newHash[1] = '';
				newHash[0] = newHash[0].length > 1 ? newHash[0] : (baseParts.hash && baseParts.hash.split('!')[0] || '');
				if (typeof newHash[1] == 'undefined') {
					newHash[1] = baseParts.hash && baseParts.hash.split('!')[1] || '';
				}
				else if (newHash[1][0] == '&') {
					newHash[1] = newHash[1].substr(1).split('&');
					var currentHashParams = this.getHashParams();
					for (var i = 0; i < newHash[1].length; i++) {
						var s = newHash[1][i].split('=', 2);
						LocationHelper.setValueByPath(currentHashParams, s[0], s[1]);
					}
					newHash[1] = this.buildParams(currentHashParams);
				}
				if (!newHash[1])
					newHash = [ newHash[0] ];
				if (newHash[1] && !newHash[0])
					newHash[0] = '#';
				parts.hash = newHash[0] == '#' && !newHash[1] ? '' : newHash.join('!');
			}
		}

		var s = [
			parts.path || '',
			parts.search || '',
			parts.hash || '',
		];

		if (parts.protocol == 'file:') {
			s.unshift(parts.protocol + '//');
		}
		else if (parts.host && parts.protocol) {
			if (parts.port)
				s.unshift(':' + parts.port);
			s.unshift(parts.host);
			s.unshift(parts.protocol + '//');
		}

		return s.join('');
	};

	LocationHelper.prototype.buildParams = function (o, parent) {
		if (typeof o != 'object')
			return parent + '=' + encodeURIComponent(o);

		var s = [];
		if (o instanceof Array) {
			for (var i = 0; i < o.length; i++) {
				var v = o[i];
				var path = parent ? parent + '[]' : '[]';
				s.push(this.buildParams(v, path));
			}
		}
		else {
			for (var k in o) {
				var v = o[k];
				var path = parent ? parent + '[' + encodeURIComponent(k) + ']' : encodeURIComponent(k);
				s.push(this.buildParams(v, path));
			}
		}
		return s.join('&');
	};

	LocationHelper.prototype.getHref = function () {
		return document.location.href;
	};

	LocationHelper.prototype.getHash = function () {
		return document.location.hash;
	};

	LocationHelper.prototype.getSearch = function () {
		return document.location.search;
	};

	LocationHelper.prototype.getPath = function () {
		return document.location.pathname;
	};

	LocationHelper.prototype.getHashParams = function () {
		var s = this.getHash();
		return this.parseParams(s.split('!', 2)[1] || '');
	};

	LocationHelper.prototype.getSearchParams = function () {
		var s = this.getSearch();
		return this.parseParams(s.slice(1));
	};

	LocationHelper.prototype.setLocation = function (hrefOrParts, replace, originalEvent) {
		var parts = typeof hrefOrParts == 'object' ? hrefOrParts : this.parseURL(hrefOrParts);
		var curParts = this.parseURL(this.getHref());
		var href = this.buildURL(parts, curParts);

		history[replace ? 'replaceState' : 'pushState']({
			href: href,
			parts: parts,
		}, '', href);

		this.trigger('location-change', {
			href: href,
			parts: parts,
			originalEvent: originalEvent,
		});

		return this;
	};

	LocationHelper.prototype.reload = function (originalEvent) {
		var href = this.getHref();
		var parts = this.parseURL(href);

		history['replaceState']({
			href: href,
			parts: parts,
		}, '', href);

		this.trigger('location-change', {
			href: href,
			parts: parts,
			originalEvent: originalEvent,
		});

		return this;
	};

	var bindParams = false;

	LocationHelper.prototype.bind = function (_bindParams) {
		_bindParams = _bindParams || true;
		if (!history || !history.pushState)
			return this;

		if (bindParams) {
			bindParams = _bindParams;
			return this;
		}
		bindParams = _bindParams;

		this.setLocation(document.location.href, true);
		var _this = this;

		window.addEventListener('popstate', function (event) {
			if (!event.state || !event.state.href || !event.state.parts)
				return;
			_this.trigger('location-change', {
				href: event.state.href,
				parts: event.state.parts,
				originalEvent: event,
			});
		}, false);

		document.body.addEventListener('click', function (event) {
			var a = event.target;
			while (a && a.nodeName != 'A')
				a = a.parentElement;
			if (a && a.target == '_null') {
				event.preventDefault();
				return false;
			}
			if (!a || !a.href || (a.target || '_self') != '_self')
				return;

			var parts = _this.parseURL(a.href);
			if (!parts || !/^https?:/.test(parts.protocol))
				return;

			var curParts = _this.parseURL(_this.getHref());
			if (parts.host != curParts.host || parts.protocol != curParts.protocol || parts.port != curParts.port)
				return;

			if (bindParams !== true && bindParams.path && curParts.path != parts.path)
				return;
			if (bindParams !== true && bindParams.search && curParts.search != parts.search)
				return;
			if (bindParams !== true && bindParams.hash && curParts.hash != parts.hash)
				return;

			_this.setLocation(parts, false, event);

			event.preventDefault();
		}, false);

		return this;
	}

	return new LocationHelper();
})();
<|endoftext|>


<commit_before>// IDEAS

// Max window size:
// 	http://bl.ocks.org/raffazizzi/3691274

// Disable clicking outside the brush
// 	http://stackoverflow.com/questions/18036836/disable-clearing-of-d3-js-brush

(D3nome = function(config) { this.init(config); }).prototype = {

	// config should include chromosome data?
	init: function(config) {

		this.maxBrushRange = 2500000;
		this.minBrushRange = 25000;

		// Set this to config.initialBPRange
		// Storing the extent locally is a hack that kills 2 birds with 1 stone
		//  - lets us constrain the box size
		//  - lets us disable the clear functionality
		this.brushExtent = [0, this.minBrushRange];

		this.config = config;
		this.chromosomes = this.config.chromosomes;

		// select the first chromosome
		this.selectedChromosome = config.selectedChromosome;

		// function for fetching and parsing data into the right format.
		this.fetchData = this.config.fetchData

		// total dimensions of the browser
		// TODO use config values
		this.totDims = {x: 898, y: 300}

		// height of nav bar area
		this.navHeight = 40;

		this.brush = null;
		this.zoom = null;

		this.viewXScale = null;
		this.viewYScale = null;
		this.viewXAxis = null;

		this.navXScale = null;
		this.navYScale = null;
		this.navXAxis = null;

		this.viewElement = null;
		this.navBoxNode = null;

		this.data = null;

		this.xFormat = function (d) {
	        var prefix = d3.formatPrefix(d);
	        return prefix.scale(d) + prefix.symbol + "b";
	    }

		this.draw();
	},

	draw: function() {
		var buf =
			"Select your chromosome:"+
	        "<select id=\"chromosome-selector\">";

	    var chromosomes = this.chromosomes;

	    for (var i = 0; i < chromosomes.length; i++) {
	    	var selected = i == this.selectedChromosome ? "selected=\"selected\"" : "";
    	buf +=
	    		"<option value=\""+chromosomes[i].id+"\" "+selected+">"+
    		 		chromosomes[i].id+" ("+chromosomes[i].length+" bp)"+
    			"</option>";
	    }

		buf += "</select>
";
		buf += "<svg id=\"d3nome-canvas\"></svg>"

		$(this.config.container).html(buf);

		this.initViewer()
	},

	// Set up the chromosome scrollbar.
	initViewer: function() {

		var totDims = this.totDims;

		var navDims = {x: totDims.x, y: this.navHeight};
		// dimensions of view genome browser area
		var viewDims = {x: 898, y: (totDims.y - navDims.y)};

		var bp = this.chromosomes[this.selectedChromosome].length

		var svg = d3.select("#d3nome-canvas")
			.attr("width", totDims.x)
		    .attr("height", totDims.y)

		// view scales
		this.viewXScale = d3.scale.linear()
	        .domain([0, bp]) // this should correspond to bp
	        .range([0, navDims.x])

	    this.viewYScale = d3.scale.linear()
	        .range([navDims.y, 0]);

	    // nav scales
		this.navXScale = d3.scale.linear()
	        .domain([0, bp]) // this should correspond to bp
	        .range([0, navDims.x]);

	    this.navYScale = d3.scale.linear()
	        .range([navDims.y, 0]);

	    // nav axis
       	this.navXAxis = d3.svg.axis()
		    .scale(this.navXScale)
		    .orient("bottom")
		    .ticks(8)
			.tickFormat(this.xFormat);

		// view axis
		this.viewXAxis = d3.svg.axis()
		    .scale(this.viewXScale)
		    .orient("bottom")
		    .ticks(8)
		    .tickFormat(this.xFormat);

		// create view element

		this.zoom = d3.behavior.zoom()
			.x(this.viewXScale)
			.size([viewDims.x, viewDims.y])
			.on('zoom', $.proxy(this.onZoom, this))
			.on('zoomend', $.proxy(this.onZoomEnd, this))

		this.zoomTranslate = this.zoom.translate();
		this.zoomScale = this.zoom.scale();

		this.viewElement = svg.append("g")
			.attr("class", "d3nome-view")
			.attr("width", viewDims.x)
			.attr("height", viewDims.y)
			.attr("transform", "translate("+0+","+0+")")

		// view chart area - add x axis
		this.viewElement.append("g")
		    .attr("class", "x axis")
		    .attr("width", navDims.x)
		    .attr("height", navDims.y)
		    .attr("transform", "translate("+0+","+(viewDims.y - navDims.y)+")")
		    .call(this.viewXAxis)

		// Append an invisible overlay rectangle to recieve zoom commands
		svg.append("rect")
			.attr("class", "d3nome-overlay")
			.attr("width", totDims.x)
			.attr("height", totDims.y)
			.call(this.zoom);

		// create the viewport, i.e. the brush
		this.brush = d3.svg.brush()
		    .x(this.navXScale)

		    // attach brush size change event handler
		    .on("brush", $.proxy(this.onBrush, this))

		    // on mouseup, load data.
		    .on("brushend", $.proxy(function() {
		    	this.loadData();
			}, this));

		// navbar - add brush
		this.navBoxNode = svg.append("g")
		    .attr("class", "d3nome-viewport")
		    .call(this.brush);
		this.navBoxNode
		    .selectAll("rect")
		    .attr("height", navDims.y)
		    .attr("transform", "translate("+0+","+viewDims.y+")")

	 	// navbar - add x axis
		svg.append("g")
		    .attr("class", "x axis")
		    .attr("id", "navbar-x-axis")
		    .attr("width", navDims.x)
		    .attr("height", navDims.y)
		    .attr("transform", "translate(0," + viewDims.y + ")")
		    .call(this.navXAxis)

		this.updateBrush(this.brushExtent);

		this.loadData(this.chromosomes[this.selectedChromosome].id, this.brushExtent[0], this.brushExtent[1]);
	},

	onBrush: function() {

    	// if brush is empty, use the this.brushExtent - i.e. the previous brush value
    	// otherwise grab the new extent from the brush.

    	// using this.brushExtent when empty disables the clearing behaviour
    	var domain = this.brush.empty() ? this.brushExtent : this.brush.extent()

		domain[0] = Math.round(domain[0])
    	domain[1] = Math.round(domain[1]);

    	// enforce minimum and maximum constraints on the extent
    	// domain = this.restrictDomainSize(this.brushExtent, domain);

    	// store the extent data for comparison later.
    	this.brushExtent = domain;
    	this.updateBrush(domain);

    	// redraw the data
    	this.drawData();
	},

	onZoom: function() {
		// this is all about prevent dragging past the boundary.
		var bp = this.chromosomes[this.selectedChromosome].length

		var domain = this.viewXScale.domain();
		console.log("Got domain "+domain); // new domain is changed back to old value

		// need a more specialised restrictDomainSize here
		var range = domain[1] - domain[0];

		var hitBoundary = false;

		if (domain[0] <= 0 && domain[1] >= bp) {
			// console.log("a");
			range = domain[1] - domain[0];
			domain = [0, bp];
			this.zoom.translate([0, 0]);
			this.zoom.scale(this.zoom.scale() * (range / bp));

		} else if (domain[0] <= 0) {
			// console.log("b");
			var offset = -domain[0];
			domain = [domain[0] + offset, domain[1] + offset];
			this.zoom.translate([0, 0]);

		} else if (domain[1] >= bp) {
			// console.log("c");
			var offset = domain[1] - bp; // positive number
			domain = [domain[0] - offset, domain[1] - offset];
			var scaledTransX = domain[0] / bp;
			this.zoom.translate([scaledTransX, 0]);
		}

	    // update the brush's version of the extent
	    this.brush.extent(domain);

	    // update the view X scale domain with the new data
		this.viewXScale.domain(domain);

		// keep track of our local extent data
		// Rename to navBoundaries

		// console.log("Setting domain: "+domain);
		this.brushExtent = domain;

		// update the view X axis as well
		this.viewElement.select(".x.axis").call(this.viewXAxis);

		// tell d3 to redraw the brush - this is important!
	    this.navBoxNode.call(this.brush);

		this.drawData();
	},

	onZoomEnd: function() {
		this.loadData();
		this.zoom.x(this.viewXScale); // this fixes a shit load of issues!
	},

	updateBrush: function(domain) {
	    // update the brush's version of the extent
	    this.brush.extent(domain);

	    // update the view X scale domain with the new data

	    console.log("updateBrush domain: "+domain);

		this.viewXScale.domain(domain);

		// must update the zoom as well
		this.zoom.x(this.viewXScale);

		// update the view X axis as well
		this.viewElement.select(".x.axis").call(this.viewXAxis);

		// tell d3 to redraw the brush - this is important!
	    this.navBoxNode.call(this.brush);
	},

	loadData: function() {
		// fetch int value of chr

		var chrID = this.chromosomes[this.selectedChromosome].id
    	var start = Math.round(this.brushExtent[0]);
    	var end = Math.round(this.brushExtent[1]);

		var chrNum = chrID[3];
		var url = this.config.dataUrl+"?chr="+chrNum+"&start="+start+"&end="+end;

		$.ajax({
			url: url,
			context: this
		}).done($.proxy(function(results) {
			this.parseData($.parseJSON(results));
		}, this));
	},

	parseData: function(data) {
		this.data = data;

		// do cool stuff with the data here.
		// maybs arrange into lanes for overlapping transcripts

		this.drawData();
	},

	drawData: function() {
		// Remove old elements
		this.viewElement.selectAll("rect").remove()

		// Add new elements
		var element = this.viewElement.selectAll(".d3nome-view")
		element
			.data(this.data).enter() // select missing nodes
			.append("rect")
			.attr("class", "d3nome-feature")

			.attr("x", $.proxy(function(d, i) { return this.viewXScale(d.start); }, this))
			.attr("y", function(d, i) { return 50; })

		    .attr("width", $.proxy(function(d, i) {
		    	return (this.viewXScale(d.end) - this.viewXScale(d.start));
		    }, this))
		    .attr("height", 30)
	}
}

<commit_msg>Cleanup<commit_after>/**
 * D3nome: D3-based genome browser
 * Matthew Norris 2015
 */
(D3nome = function(config) { this.init(config); }).prototype = {

	// config should include chromosome data?
	init: function(config) {

		this.maxBrushRange = 2500000;
		this.minBrushRange = 25000;

		// Set this to config.initialBPRange
		// Storing the extent locally is a hack that kills 2 birds with 1 stone
		//  - lets us constrain the box size
		//  - lets us disable the clear functionality
		this.navBoundaries = [0, this.minBrushRange];

		this.config = config;
		this.chromosomes = this.config.chromosomes;

		// select the first chromosome
		this.selectedChromosome = config.selectedChromosome;

		// function for fetching and parsing data into the right format.
		this.fetchData = this.config.fetchData

		// total dimensions of the browser
		// TODO use config values
		this.totDims = {x: 898, y: 300}

		// height of nav bar area
		this.navHeight = 40;

		this.brush = null;
		this.zoom = null;

		this.viewXScale = null;
		this.viewYScale = null;
		this.viewXAxis = null;

		this.navXScale = null;
		this.navYScale = null;
		this.navXAxis = null;

		this.viewElement = null;
		this.navBoxNode = null;

		this.data = null;

		this.xFormat = function (d) {
	        var prefix = d3.formatPrefix(d);
	        return prefix.scale(d) + prefix.symbol + "b";
	    }

		this.draw();
	},

	draw: function() {
		var buf =
			"Select your chromosome:"+
	        "<select id=\"chromosome-selector\">";

	    var chromosomes = this.chromosomes;

	    for (var i = 0; i < chromosomes.length; i++) {
	    	var selected = i == this.selectedChromosome ? "selected=\"selected\"" : "";
    	buf +=
	    		"<option value=\""+chromosomes[i].id+"\" "+selected+">"+
    		 		chromosomes[i].id+" ("+chromosomes[i].length+" bp)"+
    			"</option>";
	    }

		buf += "</select>
";
		buf += "<svg id=\"d3nome-canvas\"></svg>"

		$(this.config.container).html(buf);

		this.initViewer()
	},

	// Set up the chromosome scrollbar.
	initViewer: function() {

		var totDims = this.totDims;

		var navDims = {x: totDims.x, y: this.navHeight};
		// dimensions of view genome browser area
		var viewDims = {x: 898, y: (totDims.y - navDims.y)};

		var bp = this.chromosomes[this.selectedChromosome].length

		var svg = d3.select("#d3nome-canvas")
			.attr("width", totDims.x)
		    .attr("height", totDims.y)

		// view scales
		this.viewXScale = d3.scale.linear()
	        .domain([0, bp]) // this should correspond to bp
	        .range([0, navDims.x])

	    this.viewYScale = d3.scale.linear()
	        .range([navDims.y, 0]);

	    // nav scales
		this.navXScale = d3.scale.linear()
	        .domain([0, bp]) // this should correspond to bp
	        .range([0, navDims.x]);

	    this.navYScale = d3.scale.linear()
	        .range([navDims.y, 0]);

	    // nav axis
       	this.navXAxis = d3.svg.axis()
		    .scale(this.navXScale)
		    .orient("bottom")
		    .ticks(8)
			.tickFormat(this.xFormat);

		// view axis
		this.viewXAxis = d3.svg.axis()
		    .scale(this.viewXScale)
		    .orient("bottom")
		    .ticks(8)
		    .tickFormat(this.xFormat);

		// create view element

		this.zoom = d3.behavior.zoom()
			.x(this.viewXScale)
			.size([viewDims.x, viewDims.y])
			.on('zoom', $.proxy(this.onZoom, this))
			.on('zoomend', $.proxy(this.onZoomEnd, this))

		this.zoomTranslate = this.zoom.translate();
		this.zoomScale = this.zoom.scale();

		this.viewElement = svg.append("g")
			.attr("class", "d3nome-view")
			.attr("width", viewDims.x)
			.attr("height", viewDims.y)
			.attr("transform", "translate("+0+","+0+")")

		// view chart area - add x axis
		this.viewElement.append("g")
		    .attr("class", "x axis")
		    .attr("width", navDims.x)
		    .attr("height", navDims.y)
		    .attr("transform", "translate("+0+","+(viewDims.y - navDims.y)+")")
		    .call(this.viewXAxis)

		// Append an invisible overlay rectangle to recieve zoom commands
		svg.append("rect")
			.attr("class", "d3nome-overlay")
			.attr("width", totDims.x)
			.attr("height", totDims.y)
			.call(this.zoom);

		// create the viewport, i.e. the brush
		this.brush = d3.svg.brush()
		    .x(this.navXScale)

		    // attach brush size change event handler
		    .on("brush", $.proxy(this.onBrush, this))

		    // on mouseup, load data.
		    .on("brushend", $.proxy(function() {
		    	this.loadData();
			}, this));

		// navbar - add brush
		this.navBoxNode = svg.append("g")
		    .attr("class", "d3nome-viewport")
		    .call(this.brush);
		this.navBoxNode
		    .selectAll("rect")
		    .attr("height", navDims.y)
		    .attr("transform", "translate("+0+","+viewDims.y+")")

	 	// navbar - add x axis
		svg.append("g")
		    .attr("class", "x axis")
		    .attr("id", "navbar-x-axis")
		    .attr("width", navDims.x)
		    .attr("height", navDims.y)
		    .attr("transform", "translate(0," + viewDims.y + ")")
		    .call(this.navXAxis)

		this.updateBrush(this.navBoundaries);

		this.loadData(this.chromosomes[this.selectedChromosome].id, this.navBoundaries[0], this.navBoundaries[1]);
	},

	onBrush: function() {

    	// if brush is empty, use the this.navBoundaries - i.e. the previous brush value
    	// otherwise grab the new extent from the brush.

    	// using this.navBoundaries when empty disables the clearing behaviour
    	var domain = this.brush.empty() ? this.navBoundaries : this.brush.extent()

		domain[0] = Math.round(domain[0])
    	domain[1] = Math.round(domain[1]);

    	// store the extent data for comparison later.
    	this.navBoundaries = domain;
    	this.updateBrush(domain);

    	// redraw the data
    	this.drawData();
	},

	onZoom: function() {
		// this is all about prevent dragging past the boundary.
		var bp = this.chromosomes[this.selectedChromosome].length

		var domain = this.viewXScale.domain();
		// need a more specialised restrictDomainSize here
		var range = domain[1] - domain[0];

		var hitBoundary = false;

		if (domain[0] <= 0 && domain[1] >= bp) {
			range = domain[1] - domain[0];
			domain = [0, bp];
			this.zoom.translate([0, 0]);
			this.zoom.scale(this.zoom.scale() * (range / bp));

		} else if (domain[0] <= 0) {
			var offset = -domain[0];
			domain = [domain[0] + offset, domain[1] + offset];
			this.zoom.translate([0, 0]);

		} else if (domain[1] >= bp) {
			var offset = domain[1] - bp; // positive number
			domain = [domain[0] - offset, domain[1] - offset];
			var scaledTransX = domain[0] / bp;
			this.zoom.translate([scaledTransX, 0]);
		}

	    // update the brush's version of the extent
	    this.brush.extent(domain);

	    // update the view X scale domain with the new data
		this.viewXScale.domain(domain);

		// keep track of our local extent data
		// Rename to navBoundaries
		this.navBoundaries = domain;

		// update the view X axis as well
		this.viewElement.select(".x.axis").call(this.viewXAxis);

		// tell d3 to redraw the brush - this is important!
	    this.navBoxNode.call(this.brush);

		this.drawData();
	},

	onZoomEnd: function() {
		this.loadData();
		this.zoom.x(this.viewXScale); // this fixes a shit load of issues!
	},

	updateBrush: function(domain) {
	    // update the brush's version of the extent
	    this.brush.extent(domain);

	    // update the view X scale domain with the new data
		this.viewXScale.domain(domain);

		// must update the zoom as well
		this.zoom.x(this.viewXScale);

		// update the view X axis as well
		this.viewElement.select(".x.axis").call(this.viewXAxis);

		// tell d3 to redraw the brush - this is important!
	    this.navBoxNode.call(this.brush);
	},

	loadData: function() {
		// fetch int value of chr

		var chrID = this.chromosomes[this.selectedChromosome].id
    	var start = Math.round(this.navBoundaries[0]);
    	var end = Math.round(this.navBoundaries[1]);

		var chrNum = chrID[3];
		var url = this.config.dataUrl+"?chr="+chrNum+"&start="+start+"&end="+end;

		$.ajax({
			url: url,
			context: this
		}).done($.proxy(function(results) {
			this.parseData($.parseJSON(results));
		}, this));
	},

	parseData: function(data) {
		this.data = data;

		// do cool stuff with the data here.
		// maybs arrange into lanes for overlapping transcripts

		this.drawData();
	},

	drawData: function() {
		// Remove old elements
		this.viewElement.selectAll("rect").remove()

		// Add new elements
		var element = this.viewElement.selectAll(".d3nome-view")
		element
			.data(this.data).enter() // select missing nodes
			.append("rect")
			.attr("class", "d3nome-feature")

			.attr("x", $.proxy(function(d, i) { return this.viewXScale(d.start); }, this))
			.attr("y", function(d, i) { return 50; })

		    .attr("width", $.proxy(function(d, i) {
		    	return (this.viewXScale(d.end) - this.viewXScale(d.start));
		    }, this))
		    .attr("height", 30)
	}
}

<|endoftext|>


<commit_before>
//
// Turn multiple value selects into
// auto-complete lists. This is stolen
// wholesale from Qubit (with minor adjustments
// to remove Drupally bits.) Kudos to whoever
// wrote the original, which is nicer than
// this re-implementation.
//

jQuery(document).ready(function($) {

    $.fn.multiSelect = function(options) {

        function handleOption($option, name, $list) {
            $option.prop("disabled", true);
            $("
")
                .html($option.html())
                .append($("<input type='hidden' name='" + name + "' />")
                        .attr("value", $option.val()))
                .click(function(event) {
                    // only handle TAB events on keydown
                    $(this).hide(200, function() {
                        $(this).remove();
                        $option.prop("disabled", false);
                        $list.toggle($list.childen().length != 0);
                    });
                })
                .appendTo($list.show());
        }

        function handleSelect(select) {
            var $list = $("

");
            $list.hide().insertBefore(select);
            var name = $(select).attr("name");
            $(select)
                .prepend($("<option/>"))
                .removeAttr("name")
                .removeAttr("multiple")
                .bind("blur click keydown", function(event) {
                    // only handle TAB events on keydown
                    if ($(this).val() && (event.type != "keydown" || event.keyCode == 9)) {
                        var $opt = $(this).find("option:selected");
                        event.preventDefault();
                        handleOption($opt, name, $list);
                        $(this).val(null);
                    }
                })
                .find("option:selected").each(function(i, opt) {
                    handleOption($(opt), name, $list);
                })
                .end()
                .val(null);
        }

        this.each(function(i, elem) {
            handleSelect(elem);
        })
    }
    $("select[multiple]").multiSelect();
});
<commit_msg>Fix another uber-stupid typo.<commit_after>
//
// Turn multiple value selects into
// auto-complete lists. This is stolen
// wholesale from Qubit (with minor adjustments
// to remove Drupally bits.) Kudos to whoever
// wrote the original, which is nicer than
// this re-implementation.
//

jQuery(document).ready(function($) {

    $.fn.multiSelect = function(options) {

        function handleOption($option, name, $list) {
            $option.prop("disabled", true);
            $("
")
                .html($option.html())
                .append($("<input type='hidden' name='" + name + "' />")
                        .attr("value", $option.val()))
                .click(function(event) {
                    $(this).hide("fast", function() {
                        $(this).remove();
                        $option.prop("disabled", false);
                        $list.toggle($list.children().length != 0);
                    });
                })
                .appendTo($list.show());
        }

        function handleSelect(select) {
            var $list = $("

");
            $list.hide().insertBefore(select);
            var name = $(select).attr("name");
            $(select)
                .prepend($("<option/>"))
                .removeAttr("name")
                .removeAttr("multiple")
                .bind("blur click keydown", function(event) {
                    // only handle TAB events on keydown
                    if ($(this).val() && (event.type != "keydown" || event.keyCode == 9)) {
                        var $opt = $(this).find("option:selected");
                        event.preventDefault();
                        handleOption($opt, name, $list);
                        $(this).val(null);
                    }
                })
                .find("option:selected").each(function(i, opt) {
                    handleOption($(opt), name, $list);
                })
                .end()
                .val(null);
        }

        this.each(function(i, elem) {
            handleSelect(elem);
        })
    }
    $("select[multiple]").multiSelect();
});
<|endoftext|>


<commit_before>'use strict'

var fs = require('fs') // fs instance
var parseString = require('xml2js').parseString // xml2js method parseString instance
var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
var promise = require('promise');
var dataXML; // variable for data processing
var resultJSON; //variable for json processing

/*
 ---Bunch of functions to create a new valid json structure (relations part)
 */
function analyzeRelations(request) {
    function analyzeRefs(request) {

        var result = {};

        if (request.hasOwnProperty('comment') || request.hasOwnProperty('namespace')) {
            result.comment = request.comment;
            result.namespace = request.namespace;
        }

        result.id = request.id;

        if (request.hasOwnProperty('indexes')) {
            result.indexes = [];
            if (Array.isArray(request.indexes.index)) {
                for (var i = 0; i < request.indexes.index.length; i++) {
                    result.indexes[i] = request.indexes.index[i]
                }
            } else result.indexes[0] = request.indexes.index
        }
        return result;
    }

    var result = {};
    result.id = request.id;
    if (request.hasOwnProperty('comment')) {
        result.comment = request.comment;
    }
    result.refs = [];
    if (Array.isArray(request.refs.entryRef)) {
        for (var i = 0; i < request.refs.entryRef.length; i++) {
            result.refs[i] = analyzeRefs(request.refs.entryRef[i])
        }
    } else result.refs[0] = analyzeRefs(request.refs.entryRef)

    return result;
}

/*
 ---Bunch of functions to create a new valid json structure (main part)
 */
function analyze(request) {
    function analyzeNamespaces(request) {
        function analyzeEntities(request) {
            function analyzeAttr(request) {

                var result = {};
                result.id = request.id;
                if (request.hasOwnProperty('comment')) {
                    result.comment = request.comment;
                }
                result.type = request.type;

                return result;

            }

            var result = {};
            result.id = request.id;
            if (request.hasOwnProperty('comment')) {
                result.comment = request.comment;
            }
            result.attributes = [];
            if (Array.isArray(request.attributes.attribute)) {
                for (var i = 0; i < request.attributes.attribute.length; i++) {
                    result.attributes[i] = analyzeAttr(request.attributes.attribute[i])
                }
            } else result.attributes[0] = analyzeAttr(request.attributes.attribute)

            return result;

        }

        var result = {};

        result.id = request.id;
        if (request.hasOwnProperty('comment')) {
            result.comment = request.comment;
        }
        result.entities = [];
        if (Array.isArray(request.entities.entity)) {
            for (var i = 0; i < request.entities.entity.length; i++) {
                result.entities[i] = analyzeEntities(request.entities.entity[i])
            }
        } else result.entities[0] = analyzeEntities(request.entities.entity)

        if (request.hasOwnProperty('relations')) {
            result.relations = [];
            if (Array.isArray(request.relations.relation)) {
                for (var i = 0; i < request.relations.relation.length; i++) {
                    result.relations[i] = analyzeRelations(request.relations.relation[i])
                }
            } else result.relations[0] = analyzeRelations(request.relations.relation)
        }

        return result;
    }

    var result = {"dBEAR":{}};

    if (request.dBEAR.hasOwnProperty('comment')) {
        result.dBEAR.comment = request.dBEAR.comment;
    }
    result.dBEAR.namespaces = [];
    if (Array.isArray(request.dBEAR.namespaces.namespace)) {
        for (var i = 0; i < request.dBEAR.namespaces.namespace.length; i++) {
            result.dBEAR.namespaces[i] = analyzeNamespaces(request.dBEAR.namespaces.namespace[i])
        }
    } else result.dBEAR.namespaces[0] = analyzeNamespaces(request.dBEAR.namespaces.namespace)

    return result
}

/*
 ---Main part of convertor
 */
function Converter() {
    this.run = function (param) {
        var fileIn = param.demFileIn
        var fileOut = param.demFileOut

        var promise = new Promise(function (resolve, reject) {
            fs.readFile(fileIn, 'ascii', function (err, data) {
                if (err) {reject (err)} else resolve("The data was successfully read!")
                dataXML = data
            }).then(function() {
                parseString(dataXML, {
                        tagNameProcessors: [tagStripPrefix], // strip tag prefix
                        explicitArray: false, // remove arrays in child nodes
                        mergeAttrs: true, // attributes become child nodes
                        emptyTag: {}}, // default value of empty tag

                    function (err, result) {
                        if (err) {reject (err)} else resolve("Strings are successfully parsed!")
                        var result = analyze(result) // get a new json structure
                        resultJSON = result

                        return result // return JSON model
                    })
            }).then(function () {
                /* ded.json is writing to the same directory. Maybe we should create special folder? */
                fs.writeFile(fileOut, JSON.stringify(resultJSON,
                    function (key, value) // callable function to strip some useless nodes
                    {
                        var result = value
                        if (key == 'xmlns:tns' || key == 'xmlns:xsi' || key == 'xsi:schemaLocation') result = undefined;
                        return result;
                    }, 2))
                if (isElement(fileOut)) {
                    resolve("File was successfully written!")
                } else reject("Error!")
            })
        })
    }
}

/*
---Function to strip tag prefix
*/
function tagStripPrefix(name) {
    /* Is this function obliged? It is used once. */
    var result = name.replace(prefixMatch, '')
    return result //function to strip tag prefix
}

module['exports'] = Converter

/* TODO create test for converter
 * #Created on 21-Jul-15
 * */

/* To launch convert.js create test.js in this directory (it's already in gitignore) with this code: */

//var converter = require('./convert.js')
//var params = require('./generate/params.js')
//var cnv = new converter
//params.demFileIn = '../../sample/01_person/sample_01.dem.xml'
//params.demFileOut = 'dem.json'
//cnv.run(params)

/* Launch it with 'node test.js' */
<commit_msg>code review<commit_after>'use strict'

var fs = require('fs') // fs instance
var parseString = require('xml2js').parseString // xml2js method parseString instance
var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
var promise = require('promise');
var dataXML; // variable for data processing
var resultJSON; //variable for json processing

/*
 ---Bunch of functions to create a new valid json structure (relations part)
 */
function analyzeRelations(request) {
    function analyzeRefs(request) {

        var result = {};

        if (request.hasOwnProperty('comment') || request.hasOwnProperty('namespace')) {
            result.comment = request.comment;
            result.namespace = request.namespace;
        }

        result.id = request.id;

        if (request.hasOwnProperty('indexes')) {
            result.indexes = [];
            if (Array.isArray(request.indexes.index)) {
                for (var i = 0; i < request.indexes.index.length; i++) {
                    result.indexes[i] = request.indexes.index[i]
                }
            } else result.indexes[0] = request.indexes.index
        }
        return result;
    }

    var result = {};
    result.id = request.id;
    if (request.hasOwnProperty('comment')) {
        result.comment = request.comment;
    }
    result.refs = [];
    if (Array.isArray(request.refs.entryRef)) {
        for (var i = 0; i < request.refs.entryRef.length; i++) {
            result.refs[i] = analyzeRefs(request.refs.entryRef[i])
        }
    } else result.refs[0] = analyzeRefs(request.refs.entryRef)

    return result;
}

/*
 ---Bunch of functions to create a new valid json structure (main part)
 */
function analyze(request) {
    function analyzeNamespaces(request) {
        function analyzeEntities(request) {
            function analyzeAttr(request) {

                var result = {};
                result.id = request.id;
                if (request.hasOwnProperty('comment')) {
                    result.comment = request.comment;
                }
                result.type = request.type;

                return result;

            }

            var result = {};
            result.id = request.id;
            if (request.hasOwnProperty('comment')) {
                result.comment = request.comment;
            }
            result.attributes = [];
            if (Array.isArray(request.attributes.attribute)) {
                for (var i = 0; i < request.attributes.attribute.length; i++) {
                    result.attributes[i] = analyzeAttr(request.attributes.attribute[i])
                }
            } else result.attributes[0] = analyzeAttr(request.attributes.attribute)

            return result;

        }

        var result = {};

        result.id = request.id;
        if (request.hasOwnProperty('comment')) {
            result.comment = request.comment;
        }
        result.entities = [];
        if (Array.isArray(request.entities.entity)) {
            for (var i = 0; i < request.entities.entity.length; i++) {
                result.entities[i] = analyzeEntities(request.entities.entity[i])
            }
        } else result.entities[0] = analyzeEntities(request.entities.entity)

        if (request.hasOwnProperty('relations')) {
            result.relations = [];
            if (Array.isArray(request.relations.relation)) {
                for (var i = 0; i < request.relations.relation.length; i++) {
                    result.relations[i] = analyzeRelations(request.relations.relation[i])
                }
            } else result.relations[0] = analyzeRelations(request.relations.relation)
        }

        return result;
    }

    var result = {"dBEAR": {}};

    if (request.dBEAR.hasOwnProperty('comment')) {
        result.dBEAR.comment = request.dBEAR.comment;
    }
    result.dBEAR.namespaces = [];
    if (Array.isArray(request.dBEAR.namespaces.namespace)) {
        for (var i = 0; i < request.dBEAR.namespaces.namespace.length; i++) {
            result.dBEAR.namespaces[i] = analyzeNamespaces(request.dBEAR.namespaces.namespace[i])
        }
    } else result.dBEAR.namespaces[0] = analyzeNamespaces(request.dBEAR.namespaces.namespace)

    return result
}

/*
 ---Main part of convertor
 */
function Converter() {
    this.run = function (param) {
        var fileIn = param.demFileIn
        var fileOut = param.demFileOut

        fs.readFile(fileIn, 'ascii', function (err, data) {
            dataXML = data

            parseString(dataXML, {
                    tagNameProcessors: [tagStripPrefix], // strip tag prefix
                    explicitArray: false, // remove arrays in child nodes
                    mergeAttrs: true, // attributes become child nodes
                    emptyTag: {}
                }, // default value of empty tag

                function (err, result) {
                    var result = analyze(result) // get a new json structure
                    resultJSON = result

                    return result // return JSON model
                })

            /* ded.json is writing to the same directory. Maybe we should create special folder? */
            fs.writeFile(fileOut, JSON.stringify(resultJSON,
                function (key, value) // callable function to strip some useless nodes
                {
                    var result = value
                    if (key == 'xmlns:tns' || key == 'xmlns:xsi' || key == 'xsi:schemaLocation') result = undefined;
                    return result;
                }, 2))

        })

    }
}

/*
 ---Function to strip tag prefix
 */
function tagStripPrefix(name) {
    /* Is this function obliged? It is used once. */
    var result = name.replace(prefixMatch, '')
    return result //function to strip tag prefix
}

module['exports'] = Converter

/* TODO create test for converter
 * #Created on 21-Jul-15
 * */

/* To launch convert.js create test.js in this directory (it's already in gitignore) with this code: */

//var converter = require('./convert.js')
//var params = require('./generate/params.js')
//var cnv = new converter
//params.demFileIn = '../../sample/01_person/sample_01.dem.xml'
//params.demFileOut = 'dem.json'
//cnv.run(params)

/* Launch it with 'node test.js' */
<|endoftext|>


<commit_before>// Copyright 2014-2018, University of Colorado Boulder

/**
 * Demonstrates vegas UI components.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LevelCompletedNode = require( 'VEGAS/LevelCompletedNode' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var ScoreboardBar = require( 'VEGAS/ScoreboardBar' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var vegas = require( 'VEGAS/vegas' );

  /**
   * @constructor
   */
  function FiniteChallengesScreenView() {

    var self = this;
    ScreenView.call( this );

    // bar across the top
    var scoreboardNode = new ScoreboardBar(
      this.layoutBounds.width,
      new Property( 0 ), // challenge index
      new Property( 6 ), // challenges per level
      new Property( 0 ), // level
      new Property( 0 ), // score
      new Property( 0 ), // elapse time
      new BooleanProperty( true ), // timer enabled
      function() { console.log( 'Start Over' ); }, // callback for 'Start Over' button
      {
        font: new PhetFont( 20 ),
        leftMargin: 40, // visually aligned with left edge of challenge boxes
        rightMargin: 50, // visually aligned with right edge of challenge graph
        centerX: this.layoutBounds.centerX,
        top: this.layoutBounds.top
      } );
    this.addChild( scoreboardNode );

    // LevelCompletedNode that cycles through score values when you press 'Continue' button
    var score = 0;
    var addLevelCompletedNode = function() {
      var maxScore = 12;
      var levelCompletedNode = new LevelCompletedNode( 7, score, maxScore, 4, true, 77, 74, true, function() {
        console.log( 'continue' );
        score++;
        if ( score > maxScore ) {
          score = 0;
        }
        levelCompletedNode.detach();
        addLevelCompletedNode();
      }, {
        center: self.layoutBounds.center
      } );
      self.addChild( levelCompletedNode );
    };
    addLevelCompletedNode();
  }

  vegas.register( 'FiniteChallengesScreenView', FiniteChallengesScreenView );

  return inherit( ScreenView, FiniteChallengesScreenView );
} );<commit_msg>add score slider for ScoreboardBar #66<commit_after>// Copyright 2014-2018, University of Colorado Boulder

/**
 * Demonstrates vegas UI components.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var HSlider = require( 'SUN/HSlider' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LevelCompletedNode = require( 'VEGAS/LevelCompletedNode' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var ScoreboardBar = require( 'VEGAS/ScoreboardBar' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var Text = require( 'SCENERY/nodes/Text' );
  var vegas = require( 'VEGAS/vegas' );

  // constants
  var PERFECT_SCORE = 1000;

  /**
   * @constructor
   */
  function FiniteChallengesScreenView() {

    var self = this;
    ScreenView.call( this );

    var scoreProperty = new Property( 0 );

    // bar across the top
    var scoreboardBar = new ScoreboardBar(
      this.layoutBounds.width,
      new Property( 0 ), // challenge index
      new Property( 6 ), // challenges per level
      new Property( 0 ), // level
      scoreProperty, // score
      new Property( 0 ), // elapse time
      new BooleanProperty( true ), // timer enabled
      function() { console.log( 'Start Over' ); }, // callback for 'Start Over' button
      {
        font: new PhetFont( 20 ),
        leftMargin: 40, // visually aligned with left edge of challenge boxes
        rightMargin: 50, // visually aligned with right edge of challenge graph
        centerX: this.layoutBounds.centerX,
        top: this.layoutBounds.top
      } );
    this.addChild( scoreboardBar );

    // score slider
    var scoreSlider = new HBox( {
      left: this.layoutBounds.left + 20,
      top: scoreboardBar.bottom + 30,
      children: [
        new Text( 'Score: ', { font: new PhetFont( 20 ) } ),
        new HSlider( scoreProperty, { min: 0, max: PERFECT_SCORE } )
      ]
    } );
    this.addChild( scoreSlider );

    // LevelCompletedNode that cycles through score values when you press 'Continue' button
    var score = 0;
    var addLevelCompletedNode = function() {
      var maxScore = 12;
      var levelCompletedNode = new LevelCompletedNode( 7, score, maxScore, 4, true, 77, 74, true, function() {
        console.log( 'continue' );
        score++;
        if ( score > maxScore ) {
          score = 0;
        }
        levelCompletedNode.detach();
        addLevelCompletedNode();
      }, {
        center: self.layoutBounds.center
      } );
      self.addChild( levelCompletedNode );
    };
    addLevelCompletedNode();
  }

  vegas.register( 'FiniteChallengesScreenView', FiniteChallengesScreenView );

  return inherit( ScreenView, FiniteChallengesScreenView );
} );<|endoftext|>