<commit_before>/*
 * Copyright [1999-2014] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.VEPForm = Ensembl.Panel.ToolsForm.extend({

  init: function() {
    this.base();
    this.resetSpecies(this.elLk.form.find('input[name=default_species]').remove().val());
    this.editExisting();

    this.previewData = JSON.parse(this.params['preview_data']);
    delete this.params['preview_data'];

    var panel = this;

    // Change the input value on click of the examples link
    this.elLk.form.find('a._example_input').on('click', function(e) {
      e.preventDefault();
      panel.elLk.dataField.val($(this).find('input').val()).trigger('change');
    });

    // Preview button
    this.elLk.previewButton = panel.elLk.form.find('[name=preview]').hide().on('click', function(e) {
      e.preventDefault();
      panel.preview();
    });

    // show hide preview button acc to the text in the input field
    this.elLk.dataField = this.elLk.form.find('textarea[name=text]').on({
      'input paste keyup click change': function(e) {

        if ($(this).data('previousValue') === this.value) {
          return;
        } else {
          $(this).data('previousValue', this.value);
        }

        panel.elLk.previewButton.toggle(!!this.value.length);

        if (!!this.value.length) {

          // check format
          var format      = panel.detectFormat(this.value.split(/[\r\n]+/)[0]);
          var enablePrev  = format === 'id' || format === 'vcf' || format === 'ensembl';

          panel.elLk.previewButton.toggleClass('disabled', !enablePrev).prop('disabled', !enablePrev);
        }
      }
    });
  },

  preview: function() {
  /*
   * renders VEP results preview
   */

    // reset preview div
    if(!this.elLk.previewDiv) {
      this.elLk.previewDiv = $('
').appendTo(this.elLk.previewButton.parent());
    }
    this.elLk.previewDiv.show().empty();

    // get input, format and species
    this.previewInp = {};
    this.previewInp.input   = this.elLk.dataField.val().split(/[\r\n]+/)[0];
    this.previewInp.format  = this.detectFormat(this.previewInp.input);
    this.previewInp.species = this.elLk.speciesDropdown.find('input:checked').val();
    this.previewInp.baseURL = this.params['rest_server_url'] + '/vep/' + this.previewInp.species;
    var url;

    // this switch formats the input into URL for REST API
    switch (this.previewInp.format) {
      case "id":
        url = this.previewInp.baseURL + '/id/' + this.previewInp.input;
        break;

      case "ensembl":
        var arr = this.previewInp.input.split(/\s+/);
        url = this.previewInp.baseURL + '/region/' + arr[0] + ':' + arr[1] + '-' + arr[2] + ':' + (arr[4] && arr[4].match(/\-/) ? -1 : 1) + '/' + arr[3].replace(/[ACGTN-]\//, '');
        break;

      case "vcf":
        var arr = this.previewInp.input.split(/\s+/);
        var c = arr[0];
        var r = arr[3];
        var a = arr[4].split(',')[0];

        // we can't do e.g. structural variants
        if(!a.match(/[ACGTN]+/i)) {
          this.previewError('allele must be [ACGT]');
          return;
        }

        var s = parseInt(arr[1]);
        var e = s + (r.length - 1);

        // adjust coordinates for mismatched substitutions
        if(r.length != a.length) {
          s = s + 1;
          a = a.length === 1 ? '-' : a.substring(1);
        }

        url = this.previewInp.baseURL + '/region/' + c + ':' + s + '-' + e + ':' + 1 + '/' + a[0];
        break;

      default:
        this.previewError('Failed for ' + this.previewInp.format + ' format');
        return;
    }

    this.elLk.previewDiv.html('

');

    // do the AJAX request
    $.ajax({
      url       : url,
      type      : "GET",
      dataType  : 'json',
      context   : this,
      success   : function(results) { this.renderPreviewTable(results) },
      error     : function(jqXHR, textStatus, errorThrown) { this.previewError(jqXHR.responseJSON ? jqXHR.responseJSON.error : 'Unknown error'); }
    });
  },

  detectFormat: function(input) {
  /*
   * this detects input format from data pasted into VEP input form
   * code translated from Bio::EnsEMBL::Variation::Utils::VEP::detect_format
   */
    var data = input.split(/\s+/);

    // HGVS: ENST00000285667.3:c.1047_1048insC
    if (
      data.length === 1 &&
      data[0].match(/^([^\:]+)\:.*?([cgmrp]?)\.?([\*\-0-9]+.*)$/i)
    ) {
      return 'hgvs';
    }

    // variant identifier: rs123456
    else if (data.length === 1) {
      return 'id';
    }

    // VCF: 20  14370  rs6054257  G  A  29  0  NS=58;DP=258;AF=0.786;DB;H2  GT:GQ:DP:HQ
    else if (
      data.length >= 5 &&
      data[0].match(/(chr)?\w+/) &&
      data[1].match(/^\d+$/) &&
      data[3].match(/^[ACGTN\-\.]+$/i) &&
      typeof data[4] != 'undefined' && data[4].match(/^([\.ACGTN\-]+\,?)+$|^(\<[A-Z]+\>)$/i)
    ) {
      return 'vcf';
    }

    // pileup: chr1  60  T  A
    else if (
      data.length === 4 &&
      data[0].match(/(chr)?\w+/) &&
      data[1].match(/^\d+$/) &&
      data[2].match(/^[\*ACGTN-]+$/i) &&
      data[3].match(/^[\*ACGTNRYSWKM\+\/-]+$/i)
    ) {
      return 'pileup';
    }

    // ensembl: 20  14370  14370  A/G  +
    else if (
      data.length >= 4 &&
      data[0].match(/\w+/) &&
      data[1].match(/^\d+$/) &&
      data[2].match(/^\d+$/) &&
      data[3].match(/(ins|dup|del)|([ACGTN-]+\/[ACGTN-]+)/i)
    ) {
      return 'ensembl';
    }

    else {
      return 'unknown';
    }
  },

  renderPreviewTable: function(results) {
    var panel = this;

    if (!results || !results.length) {
      this.previewError('Invalid response from ' + this.params['rest_server_url']);
      return;
    }

    results = results[0];

    // function render table
    var generateTable = function(headers, rows) {
      return $('
')
        .html('

' + $.map(headers, function(h) { return '' + h + '	'; } ).join('') + '

')
        .append($.map(rows, function(row, i) {
          return $('
').addClass(i % 2 ? 'bg2' : 'bg1').append($.map(row, function(col) { return $('').html(col); }));
        }));
    };

    // function to render consequence type with colour and description HT
    var renderConsequence = function(con) {
      return $('<nobr>').append(
        $('').addClass('colour').css('background-color', panel.previewData[con]['colour']).html('&nbsp'),
        $('').html(' '),
        $('').attr({'class': '_ht ht margin-left', title: panel.previewData[con]['description']}).html(con)
      ).wrap('
').parent().html();
    };

    // function to render link as ZMenu link
    var renderZmenuLink = function(species, type, id, label) {
      return $('').attr({'class': 'zmenu', 'href': '/' + species + '/ZMenu/' + type + '?' + type.replace(/[a-z]/g, '').toLowerCase() + '=' + id}).html(label).wrap('
').parent().html();
    };

    // HTML for preview content
    this.elLk.previewDiv.html(
      '
' +
        '
Quick results for ' + this.previewInp.input + '
' +
        '
' +
          '
Most severe consequence: ' + renderConsequence(results['most_severe_consequence']) + '
' +
          ( results['colocated_variants']
            ? '
Colocated variants: ' + $.map(results['colocated_variants'], function(variant) {
                return renderZmenuLink(panel.previewInp.species, 'Variation', variant.id, variant.id) + (variant['minor_allele_freq'] ? ' (MAF: ' + variant['minor_allele_freq'] + ')' : '');
              }).join(', ') + '
'
            : ''
          ) +
        '
' +
      '
' +
      '
Note: the above is a preview of results using the ' +
        this.previewInp.species.replace('_', ' ') +
        ' Ensembl transcript database and does not include all data fields present in the full results set
'
    );

    // beginnings of table row
    var tableRows = [];

    // add table columns from transcript consequences
    if (results['transcript_consequences']) {
      $.merge(tableRows, $.map(results['transcript_consequences'], function(cons) {
        var cols = [];
        cols.push('
'
          + renderZmenuLink(panel.previewInp.species, 'Gene', cons.gene_id, cons.gene_symbol)
          + ': '
          + renderZmenuLink(panel.previewInp.species, 'Transcript', cons.transcript_id, cons.transcript_id)
          + '
'
          + '
Type: ' + cons.biotype + '
'
        );
        cols.push(cons.consequence_terms.map(function(a) { return renderConsequence(a) }).join(', '));

        // add details column
        var details = [];
        if (cons['amino_acids']) {
          details.push('Amino acids: ' + cons.amino_acids);
        }
        if (cons['sift_prediction']) {
          details.push('SIFT: ' + cons.sift_prediction);
        }
        if (cons['polyphen_prediction']) {
          details.push('PolyPhen: ' + cons.polyphen_prediction);
        }
        if (cons['distance']) {
          details.push('Distance to transcript: ' + cons.distance + 'bp');
        }
        if (!details.length) {
          details = ['-'];
        }

        cols.push($.map(details, function(detail) { return '
' + detail + '
' }));

        return [ cols ];
      }));
    }

    // add table columns from regulatory consequences
    if (results['regulatory_feature_consequences']) {
      $.merge(tableRows, $.map(results['regulatory_feature_consequences'], function(cons) {
        return [
          '' + renderZmenuLink(panel.previewInp.species, 'RegulatoryFeature', cons.regulatory_feature_id, cons.regulatory_feature_id) + ''
            + '
Type: ' + cons.biotype + '',
          cons.consequence_terms.map(function(a) { return renderConsequence(a) }).join(', '),
          '-'
        ];
      }));
    }

    // add table, but only if there is data in it
    if (tableRows.length) {
      this.elLk.previewDiv.find('._preview_table').append(generateTable(['Gene/Feature/Type', 'Consequence', 'Details'], tableRows));
    }

    // add info telling user this is not the full result set
    this.elLk.previewDiv
      .find('a.zmenu').on('click', function(e) {
        e.preventDefault();
        Ensembl.EventManager.trigger('makeZMenu', this.innerHTML.replace(/\W/g, '_'), { event: e, area: {a: this}});
      }).end()
      .find('._ht').helptip().end()
      .find('._close_button').on('click', function() { panel.elLk.previewDiv.empty(); });
  },

  previewError: function(message) {
    this.elLk.previewDiv.html('
 Unable to generate preview: ' + message + '
');
  },

  populateForm: function(jobsData) {
  /*
   * Populates the form according to the provided ticket data
   */
    if (jobsData && jobsData.length) {
      jobsData = $.extend({}, jobsData[0].config, jobsData[0]);
      this.base(jobsData);

      if (jobsData['input_file_type']) {
        this.elLk.form.find('input[name=file]').parent().append('
' + ( jobsData['input_file_type'] === 'text'
          ? 'Click here to download the previously uploaded file.'
          : 'You previously uploaded a compressed file to run this job.'
        ) + '
');
      }

      if (!this.elLk.speciesDropdown.find('input:checked').length) {
        this.elLk.speciesDropdown.find('input[type=radio]').first().click();
      }

      this.resetSpecies();
    }
  },

  reset: function() {
  /*
   * Resets the form, ready to accept next job input
   */
    this.base.apply(this, arguments);
    this.elLk.form.find('._download_link').remove();
    this.resetSpecies();
    this.resetSelectToToggle();
  },

  resetSpecies: function (species) {
  /*
   * Resets the species dropdown to select the given species or simply refresh the dropdown
   */
    if (!this.elLk.speciesDropdown) {
      this.elLk.speciesDropdown = this.elLk.form.find('._sdd');
    }

    if (species) {
      this.elLk.speciesDropdown.find('input[value=' + species + ']').first().click();
    }

    this.elLk.speciesDropdown.speciesDropdown({refresh: true});
  }
});
<commit_msg>Hide preview on form reset<commit_after>/*
 * Copyright [1999-2014] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.VEPForm = Ensembl.Panel.ToolsForm.extend({

  init: function() {
    this.base();

    this.resetSpecies(this.elLk.form.find('input[name=default_species]').remove().val());

    this.previewData = JSON.parse(this.params['preview_data']);
    delete this.params['preview_data'];

    var panel = this;

    // Change the input value on click of the examples link
    this.elLk.form.find('a._example_input').on('click', function(e) {
      e.preventDefault();
      panel.elLk.dataField.val($(this).find('input').val()).trigger('change');
    });

    // Preview button
    this.elLk.previewButton = panel.elLk.form.find('[name=preview]').hide().on('click', function(e) {
      e.preventDefault();
      panel.preview();
    });

    // Preview div
    this.elLk.previewDiv = $('
').appendTo(this.elLk.previewButton.parent()).hide();

    // show hide preview button acc to the text in the input field
    this.elLk.dataField = this.elLk.form.find('textarea[name=text]').on({
      'input paste keyup click change': function(e) {

        panel.elLk.previewButton.toggle(!!this.value.length);

        if ($(this).data('previousValue') === this.value) {
          return;
        } else {
          $(this).data('previousValue', this.value);
        }

        if (!!this.value.length) {

          // check format
          var format      = panel.detectFormat(this.value.split(/[\r\n]+/)[0]);
          var enablePrev  = format === 'id' || format === 'vcf' || format === 'ensembl';

          panel.elLk.previewButton.toggleClass('disabled', !enablePrev).prop('disabled', !enablePrev);
        }
      }
    });

    this.editExisting();
  },

  preview: function() {
  /*
   * renders VEP results preview
   */

    // reset preview div
    this.elLk.previewDiv.show().empty();

    // get input, format and species
    this.previewInp = {};
    this.previewInp.input   = this.elLk.dataField.val().split(/[\r\n]+/)[0];
    this.previewInp.format  = this.detectFormat(this.previewInp.input);
    this.previewInp.species = this.elLk.speciesDropdown.find('input:checked').val();
    this.previewInp.baseURL = this.params['rest_server_url'] + '/vep/' + this.previewInp.species;
    var url;

    // this switch formats the input into URL for REST API
    switch (this.previewInp.format) {
      case "id":
        url = this.previewInp.baseURL + '/id/' + this.previewInp.input;
        break;

      case "ensembl":
        var arr = this.previewInp.input.split(/\s+/);
        url = this.previewInp.baseURL + '/region/' + arr[0] + ':' + arr[1] + '-' + arr[2] + ':' + (arr[4] && arr[4].match(/\-/) ? -1 : 1) + '/' + arr[3].replace(/[ACGTN-]\//, '');
        break;

      case "vcf":
        var arr = this.previewInp.input.split(/\s+/);
        var c = arr[0];
        var r = arr[3];
        var a = arr[4].split(',')[0];

        // we can't do e.g. structural variants
        if(!a.match(/[ACGTN]+/i)) {
          this.previewError('allele must be [ACGT]');
          return;
        }

        var s = parseInt(arr[1]);
        var e = s + (r.length - 1);

        // adjust coordinates for mismatched substitutions
        if(r.length != a.length) {
          s = s + 1;
          a = a.length === 1 ? '-' : a.substring(1);
        }

        url = this.previewInp.baseURL + '/region/' + c + ':' + s + '-' + e + ':' + 1 + '/' + a[0];
        break;

      default:
        this.previewError('Failed for ' + this.previewInp.format + ' format');
        return;
    }

    this.elLk.previewDiv.html('

');

    // do the AJAX request
    $.ajax({
      url       : url,
      type      : "GET",
      dataType  : 'json',
      context   : this,
      success   : function(results) { this.renderPreviewTable(results) },
      error     : function(jqXHR, textStatus, errorThrown) { this.previewError(jqXHR.responseJSON ? jqXHR.responseJSON.error : 'Unknown error'); }
    });
  },

  detectFormat: function(input) {
  /*
   * this detects input format from data pasted into VEP input form
   * code translated from Bio::EnsEMBL::Variation::Utils::VEP::detect_format
   */
    var data = input.split(/\s+/);

    // HGVS: ENST00000285667.3:c.1047_1048insC
    if (
      data.length === 1 &&
      data[0].match(/^([^\:]+)\:.*?([cgmrp]?)\.?([\*\-0-9]+.*)$/i)
    ) {
      return 'hgvs';
    }

    // variant identifier: rs123456
    else if (data.length === 1) {
      return 'id';
    }

    // VCF: 20  14370  rs6054257  G  A  29  0  NS=58;DP=258;AF=0.786;DB;H2  GT:GQ:DP:HQ
    else if (
      data.length >= 5 &&
      data[0].match(/(chr)?\w+/) &&
      data[1].match(/^\d+$/) &&
      data[3].match(/^[ACGTN\-\.]+$/i) &&
      typeof data[4] != 'undefined' && data[4].match(/^([\.ACGTN\-]+\,?)+$|^(\<[A-Z]+\>)$/i)
    ) {
      return 'vcf';
    }

    // pileup: chr1  60  T  A
    else if (
      data.length === 4 &&
      data[0].match(/(chr)?\w+/) &&
      data[1].match(/^\d+$/) &&
      data[2].match(/^[\*ACGTN-]+$/i) &&
      data[3].match(/^[\*ACGTNRYSWKM\+\/-]+$/i)
    ) {
      return 'pileup';
    }

    // ensembl: 20  14370  14370  A/G  +
    else if (
      data.length >= 4 &&
      data[0].match(/\w+/) &&
      data[1].match(/^\d+$/) &&
      data[2].match(/^\d+$/) &&
      data[3].match(/(ins|dup|del)|([ACGTN-]+\/[ACGTN-]+)/i)
    ) {
      return 'ensembl';
    }

    else {
      return 'unknown';
    }
  },

  renderPreviewTable: function(results) {
    var panel = this;

    if (!results || !results.length) {
      this.previewError('Invalid response from ' + this.params['rest_server_url']);
      return;
    }

    results = results[0];

    // function render table
    var generateTable = function(headers, rows) {
      return $('
')
        .html('

' + $.map(headers, function(h) { return '' + h + '	'; } ).join('') + '

')
        .append($.map(rows, function(row, i) {
          return $('
').addClass(i % 2 ? 'bg2' : 'bg1').append($.map(row, function(col) { return $('').html(col); }));
        }));
    };

    // function to render consequence type with colour and description HT
    var renderConsequence = function(con) {
      return $('<nobr>').append(
        $('').addClass('colour').css('background-color', panel.previewData[con]['colour']).html('&nbsp'),
        $('').html(' '),
        $('').attr({'class': '_ht ht margin-left', title: panel.previewData[con]['description']}).html(con)
      ).wrap('
').parent().html();
    };

    // function to render link as ZMenu link
    var renderZmenuLink = function(species, type, id, label) {
      return $('').attr({'class': 'zmenu', 'href': '/' + species + '/ZMenu/' + type + '?' + type.replace(/[a-z]/g, '').toLowerCase() + '=' + id}).html(label).wrap('
').parent().html();
    };

    // HTML for preview content
    this.elLk.previewDiv.html(
      '
' +
        '
Quick results for ' + this.previewInp.input + '
' +
        '
' +
          '
Most severe consequence: ' + renderConsequence(results['most_severe_consequence']) + '
' +
          ( results['colocated_variants']
            ? '
Colocated variants: ' + $.map(results['colocated_variants'], function(variant) {
                return renderZmenuLink(panel.previewInp.species, 'Variation', variant.id, variant.id) + (variant['minor_allele_freq'] ? ' (MAF: ' + variant['minor_allele_freq'] + ')' : '');
              }).join(', ') + '
'
            : ''
          ) +
        '
' +
      '
' +
      '
Note: the above is a preview of results using the ' +
        this.previewInp.species.replace('_', ' ') +
        ' Ensembl transcript database and does not include all data fields present in the full results set
'
    );

    // beginnings of table row
    var tableRows = [];

    // add table columns from transcript consequences
    if (results['transcript_consequences']) {
      $.merge(tableRows, $.map(results['transcript_consequences'], function(cons) {
        var cols = [];
        cols.push('
'
          + renderZmenuLink(panel.previewInp.species, 'Gene', cons.gene_id, cons.gene_symbol)
          + ': '
          + renderZmenuLink(panel.previewInp.species, 'Transcript', cons.transcript_id, cons.transcript_id)
          + '
'
          + '
Type: ' + cons.biotype + '
'
        );
        cols.push(cons.consequence_terms.map(function(a) { return renderConsequence(a) }).join(', '));

        // add details column
        var details = [];
        if (cons['amino_acids']) {
          details.push('Amino acids: ' + cons.amino_acids);
        }
        if (cons['sift_prediction']) {
          details.push('SIFT: ' + cons.sift_prediction);
        }
        if (cons['polyphen_prediction']) {
          details.push('PolyPhen: ' + cons.polyphen_prediction);
        }
        if (cons['distance']) {
          details.push('Distance to transcript: ' + cons.distance + 'bp');
        }
        if (!details.length) {
          details = ['-'];
        }

        cols.push($.map(details, function(detail) { return '
' + detail + '
' }));

        return [ cols ];
      }));
    }

    // add table columns from regulatory consequences
    if (results['regulatory_feature_consequences']) {
      $.merge(tableRows, $.map(results['regulatory_feature_consequences'], function(cons) {
        return [
          '' + renderZmenuLink(panel.previewInp.species, 'RegulatoryFeature', cons.regulatory_feature_id, cons.regulatory_feature_id) + ''
            + '
Type: ' + cons.biotype + '',
          cons.consequence_terms.map(function(a) { return renderConsequence(a) }).join(', '),
          '-'
        ];
      }));
    }

    // add table, but only if there is data in it
    if (tableRows.length) {
      this.elLk.previewDiv.find('._preview_table').append(generateTable(['Gene/Feature/Type', 'Consequence', 'Details'], tableRows));
    }

    // add info telling user this is not the full result set
    this.elLk.previewDiv
      .find('a.zmenu').on('click', function(e) {
        e.preventDefault();
        Ensembl.EventManager.trigger('makeZMenu', this.innerHTML.replace(/\W/g, '_'), { event: e, area: {a: this}});
      }).end()
      .find('._ht').helptip().end()
      .find('._close_button').on('click', function() { panel.elLk.previewDiv.empty(); });
  },

  previewError: function(message) {
    this.elLk.previewDiv.html('
 Unable to generate preview: ' + message + '
');
  },

  populateForm: function(jobsData) {
  /*
   * Populates the form according to the provided ticket data
   */
    if (jobsData && jobsData.length) {
      jobsData = $.extend({}, jobsData[0].config, jobsData[0]);
      this.base(jobsData);

      if (jobsData['input_file_type']) {
        this.elLk.form.find('input[name=file]').parent().append('
' + ( jobsData['input_file_type'] === 'text'
          ? 'Click here to download the previously uploaded file.'
          : 'You previously uploaded a compressed file to run this job.'
        ) + '
');
      }

      if (!this.elLk.speciesDropdown.find('input:checked').length) {
        this.elLk.speciesDropdown.find('input[type=radio]').first().click();
      }

      this.resetSpecies();

      this.elLk.dataField.trigger('change');
    }
  },

  reset: function() {
  /*
   * Resets the form, ready to accept next job input
   */
    this.base.apply(this, arguments);
    this.elLk.form.find('._download_link').remove();
    this.elLk.previewDiv.empty().hide();
    this.elLk.previewButton.hide();
    this.resetSpecies();
    this.resetSelectToToggle();
  },

  resetSpecies: function (species) {
  /*
   * Resets the species dropdown to select the given species or simply refresh the dropdown
   */
    if (!this.elLk.speciesDropdown) {
      this.elLk.speciesDropdown = this.elLk.form.find('._sdd');
    }

    if (species) {
      this.elLk.speciesDropdown.find('input[value=' + species + ']').first().click();
    }

    this.elLk.speciesDropdown.speciesDropdown({refresh: true});
  }
});
<|endoftext|>


<commit_before>var path = require('path');
var fs = require('fs');
var url = require('url');
var get = require('get');
var Step = require('step');
var sqlite3 = require('sqlite3');

function getMimeType(data) {
    if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E &&
        data[3] === 0x47 && data[4] === 0x0D && data[5] === 0x0A &&
        data[6] === 0x1A && data[7] === 0x0A) {
        return 'image/png';
    } else if (data[0] === 0xFF && data[1] === 0xD8 &&
        data[data.length - 2] === 0xFF && data[data.length - 1] === 0xD9) {
        return 'image/jpeg';
    } else if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 &&
        data[3] === 0x38 && (data[4] === 0x39 || data[4] === 0x37) &&
        data[5] === 0x61) {
        return 'image/gif';
    }
};

function getExpires(headers) {
    if (headers['expires']) {
        var expires = +new Date(headers['expires']);
    } else if (headers['cache-control']) {
        var expires = Date.now() + +'max-age=600'.replace('max-age=', '') * 1000;
    }
    return expires || (Date.now() + 3600 * 1000);
}

var cache = {};

module.exports = TileJSON;
require('util').inherits(TileJSON, require('events').EventEmitter)
function TileJSON(uri, callback) {
    if (typeof uri === 'string') uri = url.parse(uri);

    if (uri.protocol && uri.protocol !== 'tilejson:') {
        throw new Error('TODO: implement other protocols')
    }

    if (uri.hostname === '.') {
        delete uri.hostname;
        delete uri.host;
        uri.pathname = '.' + uri.pathname;
    }

    if (!cache[uri.pathname]) {
        cache[uri.pathname] = this;
        this._open(uri);
    }

    var tilejson = cache[uri.pathname];
    if (!tilejson.open) {
        tilejson.once('open', callback);
    } else {
        callback(null, tilejson);
    }
    return tilejson;
}

TileJSON.prototype._open = function(uri) {
    var tilejson = this;
    function error(err) {
        process.nextTick(function() {
            tilejson.emit('open', err);
        });
    }

    this.filename = uri.pathname;
    fs.watchFile(uri.pathname, function(cur, prev) {
        // Make sure we throw away this object when the file changed.
        cache[uri.pathname]._close();
        delete cache[uri.pathname];
    });

    this._deleteInterval = setInterval(this._deleteExpired.bind(this), 60000);

    Step(function() {
        fs.readFile(uri.pathname, 'utf8', this);
    }, function(err, data) {
        if (err) return error(err);
        try { tilejson.data = JSON.parse(data); }
        catch(err) { return error(err); }

        if (!tilejson.data.id) {
            tilejson.data.id = path.basename(uri.pathname, '.tilejson');
        }

        // Create an MBTiles object as cache.
        var cacheFile = uri.pathname + '.cache';
        tilejson.cache = new sqlite3.cached.Database(cacheFile, this);
    }, function(err) {
        if (err) return error(err);
        fs.readFile(__dirname + '/schema.sql', 'utf8', this);
    }, function(err, schema) {
        if (err) return error(err);
        tilejson.cache.exec(schema, this);
    }, function(err) {
        if (err) return error(err);
        tilejson._getTile = tilejson.cache.prepare('SELECT data, headers FROM tile WHERE z = ? AND x = ? AND y = ? AND expires > ?');
        tilejson._putTile = tilejson.cache.prepare('REPLACE INTO tile (z, x, y, data, headers, expires) VALUES (?, ?, ?, ?, ?, ?)');
        tilejson._getGrid = tilejson.cache.prepare('SELECT data, headers FROM grid WHERE z = ? AND x = ? AND y = ? AND expires > ?');
        tilejson._putGrid = tilejson.cache.prepare('REPLACE INTO grid (z, x, y, data, headers, expires) VALUES (?, ?, ?, ?, ?, ?)');
        tilejson.open = true;
        tilejson.emit('open', null, tilejson);
    });
}

TileJSON.prototype._close = function() {
    fs.unwatchFile(this.filename);
    clearInterval(this._deleteInterval);
};

TileJSON.prototype._deleteExpired = function() {
    this.cache.run('DELETE FROM tile WHERE expires <= ?', Date.now());
    this.cache.run('DELETE FROM grid WHERE expires <= ?', Date.now());
};

TileJSON.list = function(filepath, callback) {
    filepath = path.resolve(filepath);
    fs.readdir(filepath, function(err, files) {
        if (err) return callback(err);
        for (var result = {}, i = 0; i < files.length; i++) {
            var name = files[i].match(/^([\w-]+)\.tilejson$/);
            if (name) result[name[1]] = 'tilejson://' + path.join(filepath, name[0]);
        }
        callback(null, result);
    });
};

TileJSON.findID = function(filepath, id, callback) {
    filepath = path.resolve(filepath);
    var file = path.join(filepath, id + '.tilejson');
    fs.stat(file, function(err, stats) {
        if (err) callback(err);
        else callback(null, 'tilejson://' + file);
    });
};

TileJSON.prototype.getInfo = function(callback) {
    if (!this.data || !this.cache) callback(new Error('Tilesource not loaded'));
    else callback(null, this.data);
};

// z, x, y are XYZ coordinates.
TileJSON.prototype.getTile = function(z, x, y, callback) {
    if (!this.data || !this.cache) return callback(new Error('Tilesource not loaded'));
    if (!this.data.tiles) return callback(new Error('Tile does not exist'));

    var tilejson = this;
    tilejson._getTile.get(z, x, y, Date.now(), function(err, row) {
        if (err) return callback(err);
        if (row) return callback(null, row.data, JSON.parse(row.headers));

        // The tile wasn't found in the database or expired already.
        // Download it from the source.
        tilejson._downloadTile(z, x, y, callback);
    });
};

TileJSON.prototype._prepareURL = function(url, z, x, y) {
    return (url
        .replace(/\{z\}/g, z)
        .replace(/\{x\}/g, x)
        .replace(/\{y\}/g, (this.data.scheme === 'tms') ? (1 << z) - 1 - y : y));
};

TileJSON.prototype._downloadTile = function(z, x, y, callback) {
    var tilejson = this;

    var url = this._prepareURL(this.data.tiles[0], z, x, y);
    new get(url).asBuffer(function(err, data, headers) {
        if (err) return callback(new Error('Tile does not exist'));

        var expires = getExpires(headers);
        var modified = headers['last-modified'] ? new Date(headers['last-modified']) : new Date;
        var headers = {
            'Content-Type': getMimeType(data),
            'Last-Modified': modified,
            'ETag': headers['etag'] || (headers['content-length'] + '-' + +modified)
        };

        tilejson._putTile.run(z, x, y, data, JSON.stringify(headers), expires);
        callback(null, data, headers);
    });
};

// z, x, y are XYZ coordinates.
TileJSON.prototype.getGrid = function(z, x, y, callback) {
    if (!this.data || !this.cache) return callback(new Error('Gridsource not loaded'));
    if (!this.data.grids) return callback(new Error('Grid does not exist'));

    var tilejson = this;
    tilejson._getGrid.get(z, x, y, Date.now(), function(err, row) {
        if (err) return callback(err);
        if (row) return callback(null, JSON.parse(row.data), JSON.parse(row.headers));

        // The grid wasn't found in the database or expired already.
        // Download it from the source.
        tilejson._downloadGrid(z, x, y, callback);
    });
};

TileJSON.prototype._downloadGrid = function(z, x, y, callback) {
    var tilejson = this;

    var url = this._prepareURL(this.data.grids[0], z, x, y);
    new get(url).asString(function(err, grid, headers) {
        if (err) return callback(new Error('Grid does not exist'));

        var expires = getExpires(headers);
        var modified = headers['last-modified'] ? new Date(headers['last-modified']) : new Date;
        var headers = {
            'Content-Type': 'application/json',
            'Last-Modified': modified,
            'ETag': headers['etag'] || (headers['content-length'] + '-' + +modified)
        };

        // TODO: compression
        grid = grid.replace(/^\s*\w+\s*\(|\)\s*;?\s*$/g, '');

        tilejson._putGrid.run(z, x, y, grid, JSON.stringify(headers), expires);
        callback(null, JSON.parse(grid), headers);
    });
};
<commit_msg>add protocol register function<commit_after>var path = require('path');
var fs = require('fs');
var url = require('url');
var get = require('get');
var Step = require('step');
var sqlite3 = require('sqlite3');

function getMimeType(data) {
    if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E &&
        data[3] === 0x47 && data[4] === 0x0D && data[5] === 0x0A &&
        data[6] === 0x1A && data[7] === 0x0A) {
        return 'image/png';
    } else if (data[0] === 0xFF && data[1] === 0xD8 &&
        data[data.length - 2] === 0xFF && data[data.length - 1] === 0xD9) {
        return 'image/jpeg';
    } else if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 &&
        data[3] === 0x38 && (data[4] === 0x39 || data[4] === 0x37) &&
        data[5] === 0x61) {
        return 'image/gif';
    }
};

function getExpires(headers) {
    if (headers['expires']) {
        var expires = +new Date(headers['expires']);
    } else if (headers['cache-control']) {
        var expires = Date.now() + +'max-age=600'.replace('max-age=', '') * 1000;
    }
    return expires || (Date.now() + 3600 * 1000);
}

var cache = {};

module.exports = TileJSON;
require('util').inherits(TileJSON, require('events').EventEmitter)
function TileJSON(uri, callback) {
    if (typeof uri === 'string') uri = url.parse(uri);

    if (uri.protocol && uri.protocol !== 'tilejson:') {
        throw new Error('TODO: implement other protocols')
    }

    if (uri.hostname === '.') {
        delete uri.hostname;
        delete uri.host;
        uri.pathname = '.' + uri.pathname;
    }

    if (!cache[uri.pathname]) {
        cache[uri.pathname] = this;
        this._open(uri);
    }

    var tilejson = cache[uri.pathname];
    if (!tilejson.open) {
        tilejson.once('open', callback);
    } else {
        callback(null, tilejson);
    }
    return tilejson;
}

TileJSON.prototype._open = function(uri) {
    var tilejson = this;
    function error(err) {
        process.nextTick(function() {
            tilejson.emit('open', err);
        });
    }

    this.filename = uri.pathname;
    fs.watchFile(uri.pathname, function(cur, prev) {
        // Make sure we throw away this object when the file changed.
        cache[uri.pathname]._close();
        delete cache[uri.pathname];
    });

    this._deleteInterval = setInterval(this._deleteExpired.bind(this), 60000);

    Step(function() {
        fs.readFile(uri.pathname, 'utf8', this);
    }, function(err, data) {
        if (err) return error(err);
        try { tilejson.data = JSON.parse(data); }
        catch(err) { return error(err); }

        if (!tilejson.data.id) {
            tilejson.data.id = path.basename(uri.pathname, '.tilejson');
        }

        // Create an MBTiles object as cache.
        var cacheFile = uri.pathname + '.cache';
        tilejson.cache = new sqlite3.cached.Database(cacheFile, this);
    }, function(err) {
        if (err) return error(err);
        fs.readFile(__dirname + '/schema.sql', 'utf8', this);
    }, function(err, schema) {
        if (err) return error(err);
        tilejson.cache.exec(schema, this);
    }, function(err) {
        if (err) return error(err);
        tilejson._getTile = tilejson.cache.prepare('SELECT data, headers FROM tile WHERE z = ? AND x = ? AND y = ? AND expires > ?');
        tilejson._putTile = tilejson.cache.prepare('REPLACE INTO tile (z, x, y, data, headers, expires) VALUES (?, ?, ?, ?, ?, ?)');
        tilejson._getGrid = tilejson.cache.prepare('SELECT data, headers FROM grid WHERE z = ? AND x = ? AND y = ? AND expires > ?');
        tilejson._putGrid = tilejson.cache.prepare('REPLACE INTO grid (z, x, y, data, headers, expires) VALUES (?, ?, ?, ?, ?, ?)');
        tilejson.open = true;
        tilejson.emit('open', null, tilejson);
    });
}

TileJSON.prototype._close = function() {
    fs.unwatchFile(this.filename);
    clearInterval(this._deleteInterval);
};

TileJSON.prototype._deleteExpired = function() {
    this.cache.run('DELETE FROM tile WHERE expires <= ?', Date.now());
    this.cache.run('DELETE FROM grid WHERE expires <= ?', Date.now());
};

TileJSON.registerProtocols = function(tilelive) {
    tilelive.protocols['tilejson:'] = TileJSON;
};

TileJSON.list = function(filepath, callback) {
    filepath = path.resolve(filepath);
    fs.readdir(filepath, function(err, files) {
        if (err) return callback(err);
        for (var result = {}, i = 0; i < files.length; i++) {
            var name = files[i].match(/^([\w-]+)\.tilejson$/);
            if (name) result[name[1]] = 'tilejson://' + path.join(filepath, name[0]);
        }
        callback(null, result);
    });
};

TileJSON.findID = function(filepath, id, callback) {
    filepath = path.resolve(filepath);
    var file = path.join(filepath, id + '.tilejson');
    fs.stat(file, function(err, stats) {
        if (err) callback(err);
        else callback(null, 'tilejson://' + file);
    });
};

TileJSON.prototype.getInfo = function(callback) {
    if (!this.data || !this.cache) callback(new Error('Tilesource not loaded'));
    else callback(null, this.data);
};

// z, x, y are XYZ coordinates.
TileJSON.prototype.getTile = function(z, x, y, callback) {
    if (!this.data || !this.cache) return callback(new Error('Tilesource not loaded'));
    if (!this.data.tiles) return callback(new Error('Tile does not exist'));

    var tilejson = this;
    tilejson._getTile.get(z, x, y, Date.now(), function(err, row) {
        if (err) return callback(err);
        if (row) return callback(null, row.data, JSON.parse(row.headers));

        // The tile wasn't found in the database or expired already.
        // Download it from the source.
        tilejson._downloadTile(z, x, y, callback);
    });
};

TileJSON.prototype._prepareURL = function(url, z, x, y) {
    return (url
        .replace(/\{z\}/g, z)
        .replace(/\{x\}/g, x)
        .replace(/\{y\}/g, (this.data.scheme === 'tms') ? (1 << z) - 1 - y : y));
};

TileJSON.prototype._downloadTile = function(z, x, y, callback) {
    var tilejson = this;

    var url = this._prepareURL(this.data.tiles[0], z, x, y);
    new get(url).asBuffer(function(err, data, headers) {
        if (err) return callback(new Error('Tile does not exist'));

        var expires = getExpires(headers);
        var modified = headers['last-modified'] ? new Date(headers['last-modified']) : new Date;
        var headers = {
            'Content-Type': getMimeType(data),
            'Last-Modified': modified,
            'ETag': headers['etag'] || (headers['content-length'] + '-' + +modified)
        };

        tilejson._putTile.run(z, x, y, data, JSON.stringify(headers), expires);
        callback(null, data, headers);
    });
};

// z, x, y are XYZ coordinates.
TileJSON.prototype.getGrid = function(z, x, y, callback) {
    if (!this.data || !this.cache) return callback(new Error('Gridsource not loaded'));
    if (!this.data.grids) return callback(new Error('Grid does not exist'));

    var tilejson = this;
    tilejson._getGrid.get(z, x, y, Date.now(), function(err, row) {
        if (err) return callback(err);
        if (row) return callback(null, JSON.parse(row.data), JSON.parse(row.headers));

        // The grid wasn't found in the database or expired already.
        // Download it from the source.
        tilejson._downloadGrid(z, x, y, callback);
    });
};

TileJSON.prototype._downloadGrid = function(z, x, y, callback) {
    var tilejson = this;

    var url = this._prepareURL(this.data.grids[0], z, x, y);
    new get(url).asString(function(err, grid, headers) {
        if (err) return callback(new Error('Grid does not exist'));

        var expires = getExpires(headers);
        var modified = headers['last-modified'] ? new Date(headers['last-modified']) : new Date;
        var headers = {
            'Content-Type': 'application/json',
            'Last-Modified': modified,
            'ETag': headers['etag'] || (headers['content-length'] + '-' + +modified)
        };

        // TODO: compression
        grid = grid.replace(/^\s*\w+\s*\(|\)\s*;?\s*$/g, '');

        tilejson._putGrid.run(z, x, y, grid, JSON.stringify(headers), expires);
        callback(null, JSON.parse(grid), headers);
    });
};
<|endoftext|>


<commit_before>'use strict';

var request = require('request');

var Pin = require('./pin');

var API_ROOT = 'https://timeline-api.getpebble.com';

/**
 * Timeline
 *
 * @constructor
 * @param {Object} [opts]
 */
var Timeline = module.exports = function (opts) {
  opts = opts || {};

  this._apiKey = opts.apiKey;

  this._apiRoot = opts.apiRoot || process.env.PEBBLE_TIMELINE_API_ROOT || API_ROOT;
};

Timeline.Pin = Pin;

/**
 * Send a pin to a user
 *
 * @param {String} userToken
 * @param {Pin} pin
 * @callback {callback} callback
 */
Timeline.prototype.sendUserPin = function (userToken, pin, callback) {
  if (typeof userToken !== 'string') {
    return callback(new Error('Expected userToken to be a string.'));
  }

  if (!(pin instanceof Pin)) {
    pin = new Pin(pin);
  }

  var opts = {
    method: 'PUT',
    endpoint: '/v1/user/pins/' + pin.id,
    headers: {
      'X-User-Token': userToken
    },
    body: pin
  };

  this._request(opts, callback);
};

/**
 * Send a pin to a topic
 *
 * @param {Array} topics
 * @param {Pin} pin
 * @callback {callback} callback
 */
Timeline.prototype.sendSharedPin = function (topics, pin, callback) {
  if (!this._apiKey) {
    return callback(new Error('API Key not set.'));
  }

  if (!(topics instanceof Array)) {
    return callback(new Error('Expected topics to be an array.'));
  }

  if (!(pin instanceof Pin)) {
    pin = new Pin(pin);
  }

  var opts = {
    method: 'PUT',
    endpoint: '/v1/shared/pins/' + pin.id,
    headers: {
      'X-API-Key': this._apiKey,
      'X-PIN-Topics': topics.join(',')
    },
    body: pin
  };

  this._request(opts, callback);
};

/**
 * Subscribe to a topic
 *
 * @param {String} userToken
 * @param {String} topic
 * @callback {callback} callback
 */
Timeline.prototype.subscribe = function (userToken, topic, callback) {
  if (typeof userToken !== 'string') {
    return callback(new Error('Expected userToken to be a string.'));
  }

  if (typeof topic !== 'string') {
    return callback(new Error('Expected topic to be a string.'));
  }

  var opts = {
    method: 'POST',
    endpoint: '/v1/user/subscriptions/' + topic,
    headers: {
      'X-User-Token': userToken
    }
  };

  this._request(opts, callback);
};

/**
 * Unsubscribe from a topic
 *
 * @param {String} userToken
 * @param {String} topic
 * @callback {callback} callback
 */
Timeline.prototype.unsubscribe = function (userToken, topic, callback) {
  if (typeof userToken !== 'string') {
    return callback(new Error('Expected userToken to be a string.'));
  }

  if (typeof topic !== 'string') {
    return callback(new Error('Expected topic to be a string.'));
  }

  var opts = {
    method: 'DELETE',
    endpoint: '/v1/user/subscriptions/' + topic,
    headers: {
      'X-User-Token': userToken
    }
  };

  this._request(opts, callback);
};

Timeline.prototype._request = function(opts, callback) {
  if (typeof callback !== 'function') {
    throw new Error('Callback not set but is required.');
  }

  var reqOpts = {};

  reqOpts.method = opts.method.toUpperCase();
  reqOpts.headers = opts.headers;
  reqOpts.url = this._apiRoot + opts.endpoint;

  switch (reqOpts.method) {
    case 'PUT':
      reqOpts.body = opts.body;
      reqOpts.json = true;
      break;
  }

  request(reqOpts, function (err, res, body) {
    if (err) {
      return callback(err, body, res);
    }

    if (res.statusCode < 300) {
      return callback(null, body, res);
    }

    if (Timeline.errorCodes[res.statusCode]) {
      return callback(new Error(Timeline.errorCodes[res.statusCode]), body, res);
    }

    callback(new Error('Unknown error. Status code: ' + res.statusCode), body, res);
  });
};

Timeline.errorCodes = {
  400: 'The pin object submitted was invalid.',
  403: 'The API key submitted was invalid.',
  410: 'The user token submitted was invalid or does not exist.',
  429: 'Server is sending updates too quickly.',
  503: 'Could not save pin due to a temporary server error.',
};
<commit_msg>Check PEBBLE_TIMELINE_API_KEY env var for the API key.<commit_after>'use strict';

var request = require('request');

var Pin = require('./pin');

var API_ROOT = 'https://timeline-api.getpebble.com';

/**
 * Timeline
 *
 * @constructor
 * @param {Object} [opts]
 */
var Timeline = module.exports = function (opts) {
  opts = opts || {};

  this._apiKey = opts.apiKey || process.env.PEBBLE_TIMELINE_API_KEY;

  this._apiRoot = opts.apiRoot || process.env.PEBBLE_TIMELINE_API_ROOT || API_ROOT;
};

Timeline.Pin = Pin;

/**
 * Send a pin to a user
 *
 * @param {String} userToken
 * @param {Pin} pin
 * @callback {callback} callback
 */
Timeline.prototype.sendUserPin = function (userToken, pin, callback) {
  if (typeof userToken !== 'string') {
    return callback(new Error('Expected userToken to be a string.'));
  }

  if (!(pin instanceof Pin)) {
    pin = new Pin(pin);
  }

  var opts = {
    method: 'PUT',
    endpoint: '/v1/user/pins/' + pin.id,
    headers: {
      'X-User-Token': userToken
    },
    body: pin
  };

  this._request(opts, callback);
};

/**
 * Send a pin to a topic
 *
 * @param {Array} topics
 * @param {Pin} pin
 * @callback {callback} callback
 */
Timeline.prototype.sendSharedPin = function (topics, pin, callback) {
  if (!this._apiKey) {
    return callback(new Error('API Key not set.'));
  }

  if (!(topics instanceof Array)) {
    return callback(new Error('Expected topics to be an array.'));
  }

  if (!(pin instanceof Pin)) {
    pin = new Pin(pin);
  }

  var opts = {
    method: 'PUT',
    endpoint: '/v1/shared/pins/' + pin.id,
    headers: {
      'X-API-Key': this._apiKey,
      'X-PIN-Topics': topics.join(',')
    },
    body: pin
  };

  this._request(opts, callback);
};

/**
 * Subscribe to a topic
 *
 * @param {String} userToken
 * @param {String} topic
 * @callback {callback} callback
 */
Timeline.prototype.subscribe = function (userToken, topic, callback) {
  if (typeof userToken !== 'string') {
    return callback(new Error('Expected userToken to be a string.'));
  }

  if (typeof topic !== 'string') {
    return callback(new Error('Expected topic to be a string.'));
  }

  var opts = {
    method: 'POST',
    endpoint: '/v1/user/subscriptions/' + topic,
    headers: {
      'X-User-Token': userToken
    }
  };

  this._request(opts, callback);
};

/**
 * Unsubscribe from a topic
 *
 * @param {String} userToken
 * @param {String} topic
 * @callback {callback} callback
 */
Timeline.prototype.unsubscribe = function (userToken, topic, callback) {
  if (typeof userToken !== 'string') {
    return callback(new Error('Expected userToken to be a string.'));
  }

  if (typeof topic !== 'string') {
    return callback(new Error('Expected topic to be a string.'));
  }

  var opts = {
    method: 'DELETE',
    endpoint: '/v1/user/subscriptions/' + topic,
    headers: {
      'X-User-Token': userToken
    }
  };

  this._request(opts, callback);
};

Timeline.prototype._request = function(opts, callback) {
  if (typeof callback !== 'function') {
    throw new Error('Callback not set but is required.');
  }

  var reqOpts = {};

  reqOpts.method = opts.method.toUpperCase();
  reqOpts.headers = opts.headers;
  reqOpts.url = this._apiRoot + opts.endpoint;

  switch (reqOpts.method) {
    case 'PUT':
      reqOpts.body = opts.body;
      reqOpts.json = true;
      break;
  }

  request(reqOpts, function (err, res, body) {
    if (err) {
      return callback(err, body, res);
    }

    if (res.statusCode < 300) {
      return callback(null, body, res);
    }

    if (Timeline.errorCodes[res.statusCode]) {
      return callback(new Error(Timeline.errorCodes[res.statusCode]), body, res);
    }

    callback(new Error('Unknown error. Status code: ' + res.statusCode), body, res);
  });
};

Timeline.errorCodes = {
  400: 'The pin object submitted was invalid.',
  403: 'The API key submitted was invalid.',
  410: 'The user token submitted was invalid or does not exist.',
  429: 'Server is sending updates too quickly.',
  503: 'Could not save pin due to a temporary server error.',
};
<|endoftext|>


<commit_before>"use strict";

module.exports = tokenize;

var rules = [{
	token: elementStart,
	match: /^<([\w-]+)/,
	mode: ["text"],
	push: "element"
}, {
	token: elementClose,
	match: /^\/>/,
	mode: ["element"],
	pop: "element"
}, {
	token: elementClose,
	match: /^<\/([\w-]+)>/,
	mode: ["text"],
}, {
	token: elementEnd,
	match: /^>/,
	mode: ["element"],
	pop: "element"
}, {
	token: attributeName,
	mode: ["element"],
	match: /^(\w+)/,
}, {
	token: attributeStart,
	match: /^="/,
	mode: ["element"],
	push: "attribute"
}, {
	token: textContent,
	match: /^([^"{]+)/,
	mode: ["attribute"]
}, {
	token: expression,
	match: /^{{([^#\/(?{{}})]+)}}/,
	mode: ["attribute", "text"]
}, {
	token: attributeEnd,
	match: /^"/,
	mode: ["attribute"],
	pop: "attribute"
}, {
	token: blockStart,
	match: /^{{#(\w+)(.*)}}/,
	mode: ["text"]
}, {
	token: blockClose,
	match: /^{{\/(\w+)}}/,
	mode: ["text"]
}, {
	token: textContent,
	match: /^([^<(?{{)]+)/,
	mode: ["text"]
}];

function tokenize(template) {
	var tokens = [];
	var index = 0;
	var max = template.length;
	var modes = [];
	var mode = "text";
	var matched;

	while (index < max) {
		matched = false;
		// Get the template from the index we're currently looking at
		var sub = template.slice(index);
		// Iterate through all the rules
		for (var ruleIndex = 0, length = rules.length; ruleIndex < length; ruleIndex++) {
			var rule = rules[ruleIndex];
			// If the rule isn't meant for this mode, skip it
			if (rule.mode.indexOf(mode) === -1)
				continue;

			// Match the rule against the template
			var match = sub.match(rule.match);
			// If this rule doesn't match, continue
			if (!match)
				continue;

			// If the rule pushes to the modes, do so
			if (rule.push)
				push(rule.push);

			// If the rule pops the mode, do so
			if (rule.pop)
				pop(rule.pop);

			// Increment index to end of match
			index += match[0].length;

			// Mark that we've made a match
			matched = true;

			// Get any arguments for token creation
			var args = match.slice(1);

			// Create the token instance
			var token = rule.token.apply(null, args);

			// Add it to the result
			tokens.push(token);

			// Screw the other rules, maaaan
			break;
		}

		// If there was no match this loop, go forward a bit
		if (!matched)
			index++;
	}

	return tokens;

	function push(newMode) {
		modes.push(mode);
		mode = newMode;
	}

	function pop(oldMode) {
		if (mode !== oldMode)
			throw new Error("Parse Error: cannot pop from mode '" + mode + "' with '" + oldMode + "'");
		mode = modes.pop();
	}
}

function textContent(content) {
	return {
		type: "textContent",
		content: content
	};
}

function elementStart(name) {
	return {
		type: "elementStart",
		name: name
	};
}

function elementEnd() {
	return {
		type: "elementEnd"
	};
}

function elementClose(name) {
	return {
		type: "elementClose",
		name: name
	};
}

function attributeName(name) {
	return {
		type: "attributeName",
		name: name
	};
}

function attributeStart() {
	return {
		type: "attributeStart"
	};
}

function attributeEnd() {
	return {
		type: "attributeEnd"
	};
}

function expression(content) {
	return {
		type: "expression",
		content: content
	};
}

function blockStart(name, content) {
	return {
		type: "blockStart",
		name: name,
		content: content
	};
}

function blockClose(name) {
	return {
		type: "blockClose",
		name: name
	};
}
<commit_msg>Use lazy quantifiers for tokenizer<commit_after>"use strict";

module.exports = tokenize;

var rules = [{
	token: elementStart,
	match: /^<([\w-]+)/,
	mode: ["text"],
	push: "element"
}, {
	token: elementClose,
	match: /^\/>/,
	mode: ["element"],
	pop: "element"
}, {
	token: elementClose,
	match: /^<\/([\w-]+)>/,
	mode: ["text"],
}, {
	token: elementEnd,
	match: /^>/,
	mode: ["element"],
	pop: "element"
}, {
	token: attributeName,
	mode: ["element"],
	match: /^(\w+)/,
}, {
	token: attributeStart,
	match: /^="/,
	mode: ["element"],
	push: "attribute"
}, {
	token: textContent,
	match: /^([^"{]+)/,
	mode: ["attribute"]
}, {
	token: expression,
	match: /^{{(.*?)}}/,
	mode: ["attribute", "text"]
}, {
	token: attributeEnd,
	match: /^"/,
	mode: ["attribute"],
	pop: "attribute"
}, {
	token: blockStart,
	match: /^{{#(\w+)(.*?)}}/,
	mode: ["text"]
}, {
	token: blockClose,
	match: /^{{\/(\w+)}}/,
	mode: ["text"]
}, {
	token: textContent,
	match: /^([^<(?{{)]+)/,
	mode: ["text"]
}];

function tokenize(template) {
	var tokens = [];
	var index = 0;
	var max = template.length;
	var modes = [];
	var mode = "text";
	var matched;

	while (index < max) {
		matched = false;
		// Get the template from the index we're currently looking at
		var sub = template.slice(index);
		// Iterate through all the rules
		for (var ruleIndex = 0, length = rules.length; ruleIndex < length; ruleIndex++) {
			var rule = rules[ruleIndex];
			// If the rule isn't meant for this mode, skip it
			if (rule.mode.indexOf(mode) === -1)
				continue;

			// Match the rule against the template
			var match = sub.match(rule.match);
			// If this rule doesn't match, continue
			if (!match)
				continue;

			// If the rule pushes to the modes, do so
			if (rule.push)
				push(rule.push);

			// If the rule pops the mode, do so
			if (rule.pop)
				pop(rule.pop);

			// Increment index to end of match
			index += match[0].length;

			// Mark that we've made a match
			matched = true;

			// Get any arguments for token creation
			var args = match.slice(1);

			// Create the token instance
			var token = rule.token.apply(null, args);

			// Add it to the result
			tokens.push(token);

			// Screw the other rules, maaaan
			break;
		}

		// If there was no match this loop, go forward a bit
		if (!matched)
			index++;
	}

	return tokens;

	function push(newMode) {
		modes.push(mode);
		mode = newMode;
	}

	function pop(oldMode) {
		if (mode !== oldMode)
			throw new Error("Parse Error: cannot pop from mode '" + mode + "' with '" + oldMode + "'");
		mode = modes.pop();
	}
}

function textContent(content) {
	return {
		type: "textContent",
		content: content
	};
}

function elementStart(name) {
	return {
		type: "elementStart",
		name: name
	};
}

function elementEnd() {
	return {
		type: "elementEnd"
	};
}

function elementClose(name) {
	return {
		type: "elementClose",
		name: name
	};
}

function attributeName(name) {
	return {
		type: "attributeName",
		name: name
	};
}

function attributeStart() {
	return {
		type: "attributeStart"
	};
}

function attributeEnd() {
	return {
		type: "attributeEnd"
	};
}

function expression(content) {
	return {
		type: "expression",
		content: content
	};
}

function blockStart(name, content) {
	return {
		type: "blockStart",
		name: name,
		content: content
	};
}

function blockClose(name) {
	return {
		type: "blockClose",
		name: name
	};
}
<|endoftext|>


<commit_before>/* global self, window, console, unsafeWindow, exportFunction */
(function(){
	"use strict";

	var settings = {
		showCallingFile: false
	};
	var blockMode = {
		getContext: {
			status: "block",
			askText: "askForPermission",
			askStatus: {
				askOnce: false,
				alreadyAsked: false,
				answer: null
			}
		},
		readAPI: {
			status: "allow",
			askText: "askForReadoutPermission",
			askStatus: {
				askOnce: false,
				alreadyAsked: false,
				answer: null
			}
		}
	};

	var undef;
	var randomImage = (function(){
		var length = Math.floor(20 + Math.random() * 100);
		var bytes = "";
		for (var i = 0; i < length; i += 1){
			bytes += String.fromCharCode(Math.floor(Math.random() * 256));
		}
		return bytes;
	}());

	// changed functions
	var changedFunctions = {
		getContext: {
			mode: blockMode.getContext,
			object: unsafeWindow.HTMLCanvasElement
		},
		toDataURL: {
			mode: blockMode.readAPI,
			object: unsafeWindow.HTMLCanvasElement,
			fake: function(){
				var type = arguments[0] || "image/png";
				return "data:" + type + ";base64," + btoa(randomImage);
			}
		},
		toBlob: {
			mode: blockMode.readAPI,
			object: unsafeWindow.HTMLCanvasElement,
			fake: function(callback){
				var type = arguments[0] || "image/png";
				var blob = new window.Blob(randomImage, {type: type});
				callback(blob);
			},
			exportOptions: {allowCallbacks: true}
		},
		mozGetAsFile: {
			mode: blockMode.readAPI,
			object: unsafeWindow.HTMLCanvasElement
		},
		getImageData: {
			mode: blockMode.readAPI,
			object: unsafeWindow.CanvasRenderingContext2D,
			fake: function(sx, sy, sw, sh){
				var l = sw * sh * 4;
				var data = new Uint8ClampedArray(l);
				for (var i = 0; i < l; i += 1){
					data[i] = Math.floor(
						Math.random() * 256
					);
				}
				var imageData = new window.ImageData(sw, sh);
				imageData.data.set(cloneInto(data, unsafeWindow));
				return imageData;
			}
		},
		readPixels: {
			mode: blockMode.readAPI,
			object: unsafeWindow.WebGLRenderingContext
		}
	};

	Object.keys(changedFunctions).forEach(function(name){
		var changedFunction = changedFunctions[name];
		var original = changedFunction.object.prototype[name];
		Object.defineProperty(
			changedFunction.object.prototype,
			name,
			{
				enumerable: true,
				configureable: false,
				get: exportFunction(function(){
					var status = changedFunction.mode.status;
					if (status === "ask"){
						var askStatus = changedFunction.mode.askStatus;
						if (askStatus.askOnce && askStatus.alreadyAsked){
							// console.log("already asked");
							status = askStatus.answer;
						}
						else {
							//console.log("asking");
							var msg = _(changedFunction.mode.askText);
							if (settings.showCallingFile){
								var callers = new Error().stack.trim().split("\n");
								//console.log(callers);
								var findme = callers.shift(); // Remove us from the stack
								findme = findme.replace(/(:[0-9]+){1,2}$/, ""); // rm line & column
								// Eliminate squashed stack. stack may contain 2+ stacks, but why...
								callers = callers.filter(function(caller){
									return caller.search(findme) === -1;
								});
								msg += "\n\n" + _("sourceOutput") + ": ";
								if (settings.showCompleteCallingStack){
									msg += callers.reduce(function(stack, c){
										return stack + "\n\t" + _("stackEntryOutput", parseStackEntry(c));
									}, "");
								}
								else{
									msg += _("stackEntryOutput", parseStackEntry(callers[0]));
								}
							}
							status = window.confirm(msg) ? "allow": "block";
							askStatus.alreadyAsked = true;
							askStatus.answer = status;
							//console.log("asking (done)");
						}
					}
					switch (status){
						case "allow":
							return original;
						case "fake":
							return changedFunction.fake? exportFunction(
								changedFunction.fake,
								unsafeWindow,
								changedFunction.exportOptions
							): undef;
						case "block":
						default:
							return undef;
					}
				}, unsafeWindow)
			}
		);
	});

	// Stack parsing
	function parseStackEntry(entry){
		var m = /@(.*):(\d*):(\d*)$/.exec(entry) || ["", entry, "--", "--"];
		return {
			url: m[1],
			line: m[2],
			column: m[3],
			raw: entry
		};
	}

	// Translation
	var _ = function(name, replace){
		var str = _[name] || name;
		if (replace){
			Object.keys(replace).forEach(function(name){
				str = str.replace(new RegExp("{" + name + "}", "g"), replace[name]);
			});
		}
		return str;
	};
	self.port.on("setTranslation", function(name, translation){
		_[name] = translation;
	});

	// Communication with main.js

	function checkPDF(blocking){
		if (document.contentType.match(/\/pdf$/i)){
			self.port.emit("isPDF", blocking);
			return true;
		}
		return false;
	}

	self.port.on("block", function(force){
		if (force || !checkPDF("block")){
			blockMode.getContext.status = "block";
			blockMode.readAPI.status = "allow";
		}
	});
	self.port.on("ask", function(force, askOnce){
		if (force || !checkPDF("askVisible")){
			blockMode.getContext.status = "ask";
			blockMode.getContext.askStatus.askOnce = askOnce;
			blockMode.readAPI.status = "allow";
		}
	});
	self.port.on("blockReadout", function(force){
		if (force || !checkPDF("blockReadout")){
			blockMode.getContext.status = "allow";
			blockMode.readAPI.status = "block";
		}
	});
	self.port.on("fakeReadout", function(force){
		if (force || !checkPDF("fakeReadout")){
			blockMode.getContext.status = "allow";
			blockMode.readAPI.status = "fake";
		}
	});
	self.port.on("askReadout", function(force, askOnce){
		if (force || !checkPDF("askReadout")){
			blockMode.getContext.status = "allow";
			blockMode.readAPI.status = "ask";
			blockMode.readAPI.askStatus.askOnce = askOnce;
		}
	});
	self.port.on("unblock", function(){
		blockMode.getContext.status = "allow";
		blockMode.readAPI.status = "allow";
	});
	self.port.on("detach", function(){
		blockMode.getContext.status = "allow";
		blockMode.readAPI.status = "allow";
	});

	// settings passthrough
	self.port.on("set", function(name, value){
		settings[name] = value;
	});
}());
<commit_msg>Removed the wrong double stack issue.<commit_after>/* global self, window, console, unsafeWindow, exportFunction */
(function(){
	"use strict";

	var settings = {
		showCallingFile: false
	};
	var blockMode = {
		getContext: {
			status: "block",
			askText: "askForPermission",
			askStatus: {
				askOnce: false,
				alreadyAsked: false,
				answer: null
			}
		},
		readAPI: {
			status: "allow",
			askText: "askForReadoutPermission",
			askStatus: {
				askOnce: false,
				alreadyAsked: false,
				answer: null
			}
		}
	};

	var undef;
	var randomImage = (function(){
		var length = Math.floor(20 + Math.random() * 100);
		var bytes = "";
		for (var i = 0; i < length; i += 1){
			bytes += String.fromCharCode(Math.floor(Math.random() * 256));
		}
		return bytes;
	}());

	// changed functions
	var changedFunctions = {
		getContext: {
			mode: blockMode.getContext,
			object: unsafeWindow.HTMLCanvasElement
		},
		toDataURL: {
			mode: blockMode.readAPI,
			object: unsafeWindow.HTMLCanvasElement,
			fake: function(){
				var type = arguments[0] || "image/png";
				return "data:" + type + ";base64," + btoa(randomImage);
			}
		},
		toBlob: {
			mode: blockMode.readAPI,
			object: unsafeWindow.HTMLCanvasElement,
			fake: function(callback){
				var type = arguments[0] || "image/png";
				var blob = new window.Blob(randomImage, {type: type});
				callback(blob);
			},
			exportOptions: {allowCallbacks: true}
		},
		mozGetAsFile: {
			mode: blockMode.readAPI,
			object: unsafeWindow.HTMLCanvasElement
		},
		getImageData: {
			mode: blockMode.readAPI,
			object: unsafeWindow.CanvasRenderingContext2D,
			fake: function(sx, sy, sw, sh){
				var l = sw * sh * 4;
				var data = new Uint8ClampedArray(l);
				for (var i = 0; i < l; i += 1){
					data[i] = Math.floor(
						Math.random() * 256
					);
				}
				var imageData = new window.ImageData(sw, sh);
				imageData.data.set(cloneInto(data, unsafeWindow));
				return imageData;
			}
		},
		readPixels: {
			mode: blockMode.readAPI,
			object: unsafeWindow.WebGLRenderingContext
		}
	};

	Object.keys(changedFunctions).forEach(function(name){
		var changedFunction = changedFunctions[name];
		var original = changedFunction.object.prototype[name];
		Object.defineProperty(
			changedFunction.object.prototype,
			name,
			{
				enumerable: true,
				configureable: false,
				get: exportFunction(function(){
					var status = changedFunction.mode.status;
					if (status === "ask"){
						var askStatus = changedFunction.mode.askStatus;
						if (askStatus.askOnce && askStatus.alreadyAsked){
							// console.log("already asked");
							status = askStatus.answer;
						}
						else {
							//console.log("asking");
							var msg = _(changedFunction.mode.askText);
							if (settings.showCallingFile){
								var callers = new Error().stack.trim().split("\n");
								//console.log(callers);
								var findme = callers.shift(); // Remove us from the stack
								findme = findme.replace(/(:[0-9]+){1,2}$/, ""); // rm line & column
								// Eliminate squashed stack. stack may contain 2+ stacks, but why...
								var inDoubleStack = false;
								callers = callers.filter(function(caller){
									var doubleStackStart = caller.search(findme) !== -1;
									inDoubleStack = inDoubleStack || doubleStackStart;
									return !inDoubleStack;
								});
								msg += "\n\n" + _("sourceOutput") + ": ";
								if (settings.showCompleteCallingStack){
									msg += callers.reduce(function(stack, c){
										return stack + "\n\t" + _("stackEntryOutput", parseStackEntry(c));
									}, "");
								}
								else{
									msg += _("stackEntryOutput", parseStackEntry(callers[0]));
								}
							}
							status = window.confirm(msg) ? "allow": "block";
							askStatus.alreadyAsked = true;
							askStatus.answer = status;
							//console.log("asking (done)");
						}
					}
					switch (status){
						case "allow":
							return original;
						case "fake":
							return changedFunction.fake? exportFunction(
								changedFunction.fake,
								unsafeWindow,
								changedFunction.exportOptions
							): undef;
						case "block":
						default:
							return undef;
					}
				}, unsafeWindow)
			}
		);
	});

	// Stack parsing
	function parseStackEntry(entry){
		var m = /@(.*):(\d*):(\d*)$/.exec(entry) || ["", entry, "--", "--"];
		return {
			url: m[1],
			line: m[2],
			column: m[3],
			raw: entry
		};
	}

	// Translation
	var _ = function(name, replace){
		var str = _[name] || name;
		if (replace){
			Object.keys(replace).forEach(function(name){
				str = str.replace(new RegExp("{" + name + "}", "g"), replace[name]);
			});
		}
		return str;
	};
	self.port.on("setTranslation", function(name, translation){
		_[name] = translation;
	});

	// Communication with main.js

	function checkPDF(blocking){
		if (document.contentType.match(/\/pdf$/i)){
			self.port.emit("isPDF", blocking);
			return true;
		}
		return false;
	}

	self.port.on("block", function(force){
		if (force || !checkPDF("block")){
			blockMode.getContext.status = "block";
			blockMode.readAPI.status = "allow";
		}
	});
	self.port.on("ask", function(force, askOnce){
		if (force || !checkPDF("askVisible")){
			blockMode.getContext.status = "ask";
			blockMode.getContext.askStatus.askOnce = askOnce;
			blockMode.readAPI.status = "allow";
		}
	});
	self.port.on("blockReadout", function(force){
		if (force || !checkPDF("blockReadout")){
			blockMode.getContext.status = "allow";
			blockMode.readAPI.status = "block";
		}
	});
	self.port.on("fakeReadout", function(force){
		if (force || !checkPDF("fakeReadout")){
			blockMode.getContext.status = "allow";
			blockMode.readAPI.status = "fake";
		}
	});
	self.port.on("askReadout", function(force, askOnce){
		if (force || !checkPDF("askReadout")){
			blockMode.getContext.status = "allow";
			blockMode.readAPI.status = "ask";
			blockMode.readAPI.askStatus.askOnce = askOnce;
		}
	});
	self.port.on("unblock", function(){
		blockMode.getContext.status = "allow";
		blockMode.readAPI.status = "allow";
	});
	self.port.on("detach", function(){
		blockMode.getContext.status = "allow";
		blockMode.readAPI.status = "allow";
	});

	// settings passthrough
	self.port.on("set", function(name, value){
		settings[name] = value;
	});
}());
<|endoftext|>


<commit_before>var request = require('request');

function Trovebox(options) {
        if (!(this instanceof Trovebox)) return new Trovebox(options);

        this.host = options.host;
        this.oauth = {
            consumer_key: options.consumer_key,
            consumer_secret: options.consumer_secret,
            token: options.token,
            token_secret: options.token_secret
        };

        this.test = new TroveboxTest(this);
        this.action = new TroveboxAction(this);
        this.activity = new TroveboxActivity(this);
        this.photo = new TroveboxPhoto(this);
        this.tag = new TroveboxTag(this);
        this.group = new TroveboxGroup(this);
        this.webhook = new TroveboxWebhook(this);
}

module.exports = Trovebox;

function TroveboxTest(parent) {
    this.parent = parent;
}

function TroveboxAction(parent) {
    this.parent = parent;
}

function TroveboxActivity(parent) {
    this.parent = parent;
}

function TroveboxPhoto(parent) {
    this.parent = parent;
}

function TroveboxTag(parent) {
    this.parent = parent;
}

function TroveboxGroup(parent) {
    this.parent = parent;
}

function TroveboxWebhook(parent) {
    this.parent = parent;
}

Trovebox.prototype.get = function(req, callback) {
        _.extend(req, {json: true})
        request(req, function (err, response, body) {
                callback(err, body);
        })
}

Trovebox.prototype.post = function(req, callback) {
        _.extend(req, {json: true})
        request.post(req, function (err, response, body) {
                callback(err, body);
        })
}

// TEST

TroveboxTest.prototype.hello = function(opts, callback) {
        var request = {url:this.parent.host+'/hello.json', oauth:this.parent.oauth, qs:opts};
        this.parent.get(request, callback)
}

// PHOTO

// Convert array of tags to commaseparated string
function parseTags(tags){
    if(Array.isArray(tags)){
        return tags.join(",")
    }
    return tags
}

TroveboxPhoto.prototype.list = function(opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }

        if(opts.tags){
            opts.tags = parseTags(opts.tags);
        }
        var request = {url:this.parent.host+'/photos/list.json', oauth:this.parent.oauth, qs:opts};
        this.parent.get(request, callback)
}

TroveboxPhoto.prototype.view = function(photoid, opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }
        var request = {url:this.parent.host+'/photo/'+photoid+'/view.json', oauth:this.parent.oauth, qs:opts};
        this.parent.get(request, callback)
}

TroveboxPhoto.prototype.nextprevious = function(photoid, opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }
        var request = {url:this.parent.host+'/photo/'+photoid+'/nextprevious.json', oauth:this.parent.oauth};
        this.parent.get(request, callback)
}

// TAGS

TroveboxTag.prototype.list = function(callback) {
        var request = {url:this.parent.host+'/tags/list.json', oauth:this.parent.oauth};
        this.parent.get(request, callback)
}

// ACTIVITIES

TroveboxActivity.prototype.list = function(callback) {
        var request = {url:this.parent.host+'/activities/list.json', oauth:this.parent.oauth};
        this.parent.get(request, callback)
}
<commit_msg>Add support for uploading<commit_after>var request = require('request');
var _ = require('underscore');
var fs = require('fs');

function Trovebox(options) {
        if (!(this instanceof Trovebox)) return new Trovebox(options);

        this.host = options.host;
        this.oauth = {
            consumer_key: options.consumer_key,
            consumer_secret: options.consumer_secret,
            token: options.token,
            token_secret: options.token_secret
        };

        this.test = new TroveboxTest(this);
        this.action = new TroveboxAction(this);
        this.activity = new TroveboxActivity(this);
        this.photo = new TroveboxPhoto(this);
        this.tag = new TroveboxTag(this);
        this.group = new TroveboxGroup(this);
        this.webhook = new TroveboxWebhook(this);
}

module.exports = Trovebox;

function TroveboxTest(parent) {
    this.parent = parent;
}

function TroveboxAction(parent) {
    this.parent = parent;
}

function TroveboxActivity(parent) {
    this.parent = parent;
}

function TroveboxPhoto(parent) {
    this.parent = parent;
}

function TroveboxTag(parent) {
    this.parent = parent;
}

function TroveboxGroup(parent) {
    this.parent = parent;
}

function TroveboxWebhook(parent) {
    this.parent = parent;
}

Trovebox.prototype.get = function(req, callback) {
        _.extend(req, {json: true})
        request(req, function (err, response, body) {
                callback(err, body);
        })
}

Trovebox.prototype.post = function(req, callback) {
        _.extend(req, {json: true})

        if(req.photo){
            var r = request.post(req, function (err, response, body) {
                callback(err, body);
            })

            var form = r.form()
            var photoData = fs.createReadStream(req.photo);

            form.append('photo', photoData);

        } else{
            request.post(req, function (err, response, body) {
                callback(err, body);
            })

        }

}

// TEST

TroveboxTest.prototype.hello = function(opts, callback) {
        var request = {url:this.parent.host+'/hello.json', oauth:this.parent.oauth, qs:opts};
        this.parent.get(request, callback)
}

// PHOTO

// Convert array of tags to commaseparated string
function parseTags(tags){
    if(Array.isArray(tags)){
        return tags.join(",")
    }
    return tags
}

TroveboxPhoto.prototype.list = function(opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }

        if(opts.tags){
            opts.tags = parseTags(opts.tags);
        }
        var request = {url:this.parent.host+'/photos/list.json', oauth:this.parent.oauth, qs:opts};
        this.parent.get(request, callback)
}

TroveboxPhoto.prototype.view = function(photoid, opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }
        var request = {url:this.parent.host+'/photo/'+photoid+'/view.json', oauth:this.parent.oauth, qs:opts};
        this.parent.get(request, callback)
}

TroveboxPhoto.prototype.nextprevious = function(photoid, opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }
        var request = {url:this.parent.host+'/photo/'+photoid+'/nextprevious.json', oauth:this.parent.oauth};
        this.parent.get(request, callback)
}

TroveboxPhoto.prototype.upload = function(photo, opts, callback) {
        if (typeof callback === 'undefined') {
            callback = opts;
            opts = {};
        }

        if(opts.tags){
            opts.tags = parseTags(opts.tags);
        }

        var req = {url:this.parent.host+'/photo/upload.json', oauth:this.parent.oauth, qs:opts, photo:photo};

        this.parent.post(req, callback)
}

// TAGS

TroveboxTag.prototype.list = function(callback) {
        var request = {url:this.parent.host+'/tags/list.json', oauth:this.parent.oauth};
        this.parent.get(request, callback)
}

// ACTIVITIES

TroveboxActivity.prototype.list = function(callback) {
        var request = {url:this.parent.host+'/activities/list.json', oauth:this.parent.oauth};
        this.parent.get(request, callback)
}
<|endoftext|>


<commit_before>/*
 * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of iotagent-ul
 *
 * iotagent-ul is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * iotagent-ul is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with iotagent-ul.
 * If not, seehttp://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[iot_support@tid.es]
 */

const errors = require('./errors');
const constants = require('./constants');
const iotAgentLib = require('iotagent-node-lib');
const _ = require('underscore');

/**
 * Generates a function that parse the given attribute string, adding it to the previous collection, using the
 * provided separator.
 *
 * @param {String} separator           Character used to separate the key from the value
 * @return {Function}                  The key-pair parsing function for the separator.
 */
function addAttribute(separator) {
    return function (collection, newAttr) {
        const fields = newAttr.split(separator);

        if (!fields || fields.length !== 2) {
            throw new errors.ParseError('Extracting attribute:' + newAttr);
        } else {
            collection[fields[0]] = fields[1];
            return collection;
        }
    };
}

/**
 * Extract a key value pair from a length 2 array and add it as an attribute to the given object.
 *
 * @param {Object} collection          Collection of key value pairs
 * @param {Array} pair                 Key value pair as an array of length 2.
 * @return {Object}                    Resulting object.
 */
function addAttributePair(collection, pair) {
    if (!pair || pair.length !== 2) {
        throw new errors.ParseError('Extracting attribute:' + JSON.stringify(pair));
    } else {
        collection[pair[0]] = pair[1];
        return collection;
    }
}

/**
 * Divide a list into smaller chunks, of the given size.
 *
 * @param {Array}  list         Array to be divided.
 * @param {Number} size         Size of the chunks.
 * @return {Array}              Array containing the chunks.
 */
function chunk(list, size) {
    const chunks = [];

    while (list.length) {
        chunks.push(list.splice(0, size));
    }

    return chunks;
}

function parseMeasure(group, numberOfBars) {
    let timestamp;
    let returnValue;

    if (numberOfBars && numberOfBars.length % 2 === 0) {
        timestamp = group.substr(0, group.indexOf('|'));
        group = group.substr(group.indexOf('|') + 1);
    }

    const attributes = group.split('|');

    if (
        !attributes ||
        attributes.length === 0 ||
        attributes.length % 2 !== 0 ||
        attributes.filter(_.isEmpty).length > 0
    ) {
        throw new errors.ParseError('Parsing group:' + group);
    } else {
        returnValue = chunk(attributes, 2).reduce(addAttributePair, {});

        if (timestamp) {
            returnValue[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
        }

        return returnValue;
    }
}

/**
 * Parse a command execution payload, returning an object containing information about the command. Throws
 * an error if the syntax is not correct.
 *
 * The returned object contains three attributes:
 * - deviceId: ID of the device executing the command.
 * - command: name of the command to execute.
 * - params: object containing the parameters to the command in map format.
 *
 * @param {String} payload          Ultralight 2.0 command execution payload
 * @return {Object}                Object containing the command information
 */
function command(payload) {
    const fields = payload.split('|');
    const result = {};

    if (fields.length < 1 || fields[0].indexOf('@') < 0) {
        throw new errors.ParseError('Parsing command:' + payload);
    }

    const deviceData = fields[0].split('@');

    result.deviceId = deviceData[0];
    result.command = deviceData[1];

    const dataSection = fields.splice(1);

    if (dataSection.length === 1 && dataSection[0].indexOf('=') < 0) {
        result.value = dataSection[0];
    } else {
        result.params = dataSection.reduce(addAttribute('='), {});
    }

    return result;
}

function parseCommand(group) {
    return command(group);
}

/**
 * Parse a measure group, i.e.: a string of key-value pairs sepparated by the '|' character, returning an object with
 * the same information structured as a map.
 *
 * @param {String} group                String containing a UL2.0 codified group.
 * @return {Object}                    Object representing the information in the group.
 */
function parseGroup(group) {
    if (group[0] === '|') {
        group = group.substr(1);
    }

    const numberOfBars = group.match(/\|/g);

    if (group.indexOf('@') > 0) {
        return parseCommand(group);
    }
    return parseMeasure(group, numberOfBars);
}

/**
 * Parse a measure reporting payload, returning an array with all the measure groups restructured as objects. Throws
 * an error if the syntax is not correct.
 *
 * @param {String} payload          Ultralight 2.0 measure reporting payload
 * @return {Array}                 Array containing an object per measure group
 */
function parse(payload) {
    let groups;
    let result;

    if (!payload) {
        throw new errors.ParseError('Empty payload parsing Ultraligh 2.0');
    }

    try {
        groups = payload.split('#');
        result = groups.map(parseGroup);
    } catch (e) {
        throw new errors.ParseError('Unknown error parsing Ultralight 2.0: %s', e);
    }

    return result;
}

function parseConfigurationRequest(payload) {
    const fields = payload.split('|');

    return {
        type: fields[0],
        attributes: fields.slice(1, fields.lenght)
    };
}

/**
 * Parse a command result payload, returning an object containing information about the command result. Throws
 * an error if the syntax is not correct.
 *
 * The returned object contains three attributes:
 * - deviceId: ID of the device executing the command.
 * - command: name of the command to execute.
 * - result: a string representing the output of the command.
 *
 * @param {String} payload          Ultralight 2.0 command result payload
 * @return {Object}                Object containing the result information
 */
function result(payload) {
    const fields = payload.split('|');

    if (fields.length < 1 || fields[0].indexOf('@') < 0) {
        throw new errors.ParseError('Parsing command:' + payload);
    }

    const deviceData = fields[0].split('@');

    return {
        deviceId: deviceData[0],
        command: deviceData[1],
        result: fields[1]
    };
}

/**
 * Serializes a payload for a command depending on its payloadType if provided
 *
 * @param {String} payload          Payload to serialized
 * @param {Object} command          Command attribute
 * @return {Function}               Returns a serialized payload
 */
function serializedPayloadCommand(payload, command) {
    let serialized;
    if (command && command.payloadType) {
        switch (command.payloadType.toLowerCase()) {
            case 'binaryfromstring':
                serialized = Buffer.from(payload.toString());
                break;
            case 'binaryfromhex':
                serialized = Buffer.from(payload, 'HEX');
                break;
            case 'binaryfromjson':
                serialized = Buffer.from(JSON.stringify(payload));
                break;
            case 'json':
                serialized = JSON.stringify(payload);
                break;
            default:
                // passthrough
                serialized = payload;
        }
    } else {
        serialized = payload;
    }
    return serialized;
}

/**
 * Creates the command payload string, based on the device information and command attributes.
 *
 * @param {Object} device           Object containing all the information about a device.
 * @param {String} command          Name of the command to execute.
 * @param {Object} attributes       Object containing the command parameters as attributes of the object.
 * @return {String}                 String with the codified command.
 */
function createCommandPayload(device, command, attributes) {
    function addAttributes(current, key) {
        //let cmd = device && device.commands.find((att) => att.name === key);
        let value = attributes[key];
        // if (cmd && cmd.expression) {
        //     let parser = iotAgentLib.dataPlugins.expressionTransformation;
        //     let ctxt = device.staticAttributes.concat({key: value});
        //     value = parser.applyExpression(cmd.expression, ctxt, device);
        // }
        return current + '|' + key + '=' + value;
    }

    if (typeof attributes === 'object') {
        return Object.keys(attributes).reduce(addAttributes, device.id + '@' + command);
    }
    let cmd = device && device.commands.find((att) => att.name === command);
    if (cmd && cmd.expression) {
        let parser = iotAgentLib.dataPlugins.expressionTransformation;
        let ctxt = parser.extractContext(device.staticAttributes.concat({ command: attributes }), device);
        attributes = parser.applyExpression(cmd.expression, ctxt, device);
    }
    let payload = device.id + '@' + command + '|' + attributes;
    return serializedPayloadCommand(payload, cmd);
}

/**
 * Creates the configuration payload string, based on the device information.
 *
 * @param {Object} device           Object containing all the information about a device.
 * @param {Object} attributes       Object containing the command parameters as attributes of the object.
 * @return {String}                 String with the codified command.
 */
function createConfigurationPayload(deviceId, attributes) {
    function addAttributes(current, key) {
        return current + '|' + key + '=' + attributes[key];
    }

    return Object.keys(attributes).reduce(addAttributes, deviceId + '@configuration');
}

exports.parse = parse;
exports.parseConfigurationRequest = parseConfigurationRequest;
exports.command = command;
exports.result = result;
exports.createCommandPayload = createCommandPayload;
exports.createConfigurationPayload = createConfigurationPayload;
<commit_msg>Update ulParser.js<commit_after>/*
 * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of iotagent-ul
 *
 * iotagent-ul is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * iotagent-ul is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with iotagent-ul.
 * If not, seehttp://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[iot_support@tid.es]
 */

const errors = require('./errors');
const constants = require('./constants');
const iotAgentLib = require('iotagent-node-lib');
const _ = require('underscore');

/**
 * Generates a function that parse the given attribute string, adding it to the previous collection, using the
 * provided separator.
 *
 * @param {String} separator           Character used to separate the key from the value
 * @return {Function}                  The key-pair parsing function for the separator.
 */
function addAttribute(separator) {
    return function (collection, newAttr) {
        const fields = newAttr.split(separator);

        if (!fields || fields.length !== 2) {
            throw new errors.ParseError('Extracting attribute:' + newAttr);
        } else {
            collection[fields[0]] = fields[1];
            return collection;
        }
    };
}

/**
 * Extract a key value pair from a length 2 array and add it as an attribute to the given object.
 *
 * @param {Object} collection          Collection of key value pairs
 * @param {Array} pair                 Key value pair as an array of length 2.
 * @return {Object}                    Resulting object.
 */
function addAttributePair(collection, pair) {
    if (!pair || pair.length !== 2) {
        throw new errors.ParseError('Extracting attribute:' + JSON.stringify(pair));
    } else {
        collection[pair[0]] = pair[1];
        return collection;
    }
}

/**
 * Divide a list into smaller chunks, of the given size.
 *
 * @param {Array}  list         Array to be divided.
 * @param {Number} size         Size of the chunks.
 * @return {Array}              Array containing the chunks.
 */
function chunk(list, size) {
    const chunks = [];

    while (list.length) {
        chunks.push(list.splice(0, size));
    }

    return chunks;
}

function parseMeasure(group, numberOfBars) {
    let timestamp;
    let returnValue;

    if (numberOfBars && numberOfBars.length % 2 === 0) {
        timestamp = group.substr(0, group.indexOf('|'));
        group = group.substr(group.indexOf('|') + 1);
    }

    const attributes = group.split('|');

    if (
        !attributes ||
        attributes.length === 0 ||
        attributes.length % 2 !== 0 ||
        attributes.filter(_.isEmpty).length > 0
    ) {
        throw new errors.ParseError('Parsing group:' + group);
    } else {
        returnValue = chunk(attributes, 2).reduce(addAttributePair, {});

        if (timestamp) {
            returnValue[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
        }

        return returnValue;
    }
}

/**
 * Parse a command execution payload, returning an object containing information about the command. Throws
 * an error if the syntax is not correct.
 *
 * The returned object contains three attributes:
 * - deviceId: ID of the device executing the command.
 * - command: name of the command to execute.
 * - params: object containing the parameters to the command in map format.
 *
 * @param {String} payload          Ultralight 2.0 command execution payload
 * @return {Object}                Object containing the command information
 */
function command(payload) {
    const fields = payload.split('|');
    const result = {};

    if (fields.length < 1 || fields[0].indexOf('@') < 0) {
        throw new errors.ParseError('Parsing command:' + payload);
    }

    const deviceData = fields[0].split('@');

    result.deviceId = deviceData[0];
    result.command = deviceData[1];

    const dataSection = fields.splice(1);

    if (dataSection.length === 1 && dataSection[0].indexOf('=') < 0) {
        result.value = dataSection[0];
    } else {
        result.params = dataSection.reduce(addAttribute('='), {});
    }

    return result;
}

function parseCommand(group) {
    return command(group);
}

/**
 * Parse a measure group, i.e.: a string of key-value pairs sepparated by the '|' character, returning an object with
 * the same information structured as a map.
 *
 * @param {String} group                String containing a UL2.0 codified group.
 * @return {Object}                    Object representing the information in the group.
 */
function parseGroup(group) {
    if (group[0] === '|') {
        group = group.substr(1);
    }

    const numberOfBars = group.match(/\|/g);

    if (group.indexOf('@') > 0) {
        return parseCommand(group);
    }
    return parseMeasure(group, numberOfBars);
}

/**
 * Parse a measure reporting payload, returning an array with all the measure groups restructured as objects. Throws
 * an error if the syntax is not correct.
 *
 * @param {String} payload          Ultralight 2.0 measure reporting payload
 * @return {Array}                 Array containing an object per measure group
 */
function parse(payload) {
    let groups;
    let result;

    if (!payload) {
        throw new errors.ParseError('Empty payload parsing Ultraligh 2.0');
    }

    try {
        groups = payload.split('#');
        result = groups.map(parseGroup);
    } catch (e) {
        throw new errors.ParseError('Unknown error parsing Ultralight 2.0: %s', e);
    }

    return result;
}

function parseConfigurationRequest(payload) {
    const fields = payload.split('|');

    return {
        type: fields[0],
        attributes: fields.slice(1, fields.lenght)
    };
}

/**
 * Parse a command result payload, returning an object containing information about the command result. Throws
 * an error if the syntax is not correct.
 *
 * The returned object contains three attributes:
 * - deviceId: ID of the device executing the command.
 * - command: name of the command to execute.
 * - result: a string representing the output of the command.
 *
 * @param {String} payload          Ultralight 2.0 command result payload
 * @return {Object}                Object containing the result information
 */
function result(payload) {
    const fields = payload.split('|');

    if (fields.length < 1 || fields[0].indexOf('@') < 0) {
        throw new errors.ParseError('Parsing command:' + payload);
    }

    const deviceData = fields[0].split('@');

    return {
        deviceId: deviceData[0],
        command: deviceData[1],
        result: fields[1]
    };
}

/**
 * Serializes a payload for a command depending on its payloadType if provided
 *
 * @param {String} payload          Payload to serialized
 * @param {Object} command          Command attribute
 * @return {Function}               Returns a serialized payload
 */
function serializedPayloadCommand(payload, command) {
    let serialized;
    if (command && command.payloadType) {
        switch (command.payloadType.toLowerCase()) {
            case 'binaryfromstring':
                serialized = Buffer.from(payload.toString());
                break;
            case 'binaryfromhex':
                serialized = Buffer.from(payload, 'HEX');
                break;
            case 'binaryfromjson':
                serialized = Buffer.from(JSON.stringify(payload));
                break;
            case 'json':
                serialized = JSON.stringify(payload);
                break;
            default:
                // passthrough
                serialized = payload;
        }
    } else {
        serialized = payload;
    }
    return serialized;
}

/**
 * Creates the command payload string, based on the device information and command attributes.
 *
 * @param {Object} device           Object containing all the information about a device.
 * @param {String} command          Name of the command to execute.
 * @param {Object} attributes       Object containing the command parameters as attributes of the object.
 * @return {String}                 String with the codified command.
 */
function createCommandPayload(device, command, attributes) {
    function addAttributes(current, key) {
        let cmd = device && device.commands.find((att) => att.name === key);
        let value = attributes[key];
        if (cmd && cmd.expression) {
            let parser = iotAgentLib.dataPlugins.expressionTransformation;
            let ctxt = parser.extractContext(device.staticAttributes.concat({key: value}), device);
            value = parser.applyExpression(cmd.expression, ctxt, device);
        }
        return current + '|' + key + '=' + value;
    }

    if (typeof attributes === 'object') {
        return Object.keys(attributes).reduce(addAttributes, device.id + '@' + command);
    }
    let cmd = device && device.commands.find((att) => att.name === command);
    if (cmd && cmd.expression) {
        let parser = iotAgentLib.dataPlugins.expressionTransformation;
        let ctxt = parser.extractContext(device.staticAttributes.concat({ command: attributes }), device);
        attributes = parser.applyExpression(cmd.expression, ctxt, device);
    }
    let payload = device.id + '@' + command + '|' + attributes;
    return serializedPayloadCommand(payload, cmd);
}

/**
 * Creates the configuration payload string, based on the device information.
 *
 * @param {Object} device           Object containing all the information about a device.
 * @param {Object} attributes       Object containing the command parameters as attributes of the object.
 * @return {String}                 String with the codified command.
 */
function createConfigurationPayload(deviceId, attributes) {
    function addAttributes(current, key) {
        return current + '|' + key + '=' + attributes[key];
    }

    return Object.keys(attributes).reduce(addAttributes, deviceId + '@configuration');
}

exports.parse = parse;
exports.parseConfigurationRequest = parseConfigurationRequest;
exports.command = command;
exports.result = result;
exports.createCommandPayload = createCommandPayload;
exports.createConfigurationPayload = createConfigurationPayload;
<|endoftext|>