<commit_before>/**
 * Metabolite controller.
 *
 * Handles actions performed in metabolites views.
 * @author Stefan Verhoeven
 */
Ext.define('Esc.magmaweb.controller.Metabolites', {
  extend: 'Ext.app.Controller',
  views: [ 'metabolite.Panel' ],
  stores: [ 'Metabolites' ],
  models: [ 'Metabolite' ],
  uses: [
    'Esc.magmaweb.view.metabolite.MetabolizeForm',
    'Esc.magmaweb.view.metabolite.MetabolizeOneForm'
  ],
  refs: [{
    ref: 'metaboliteList', selector: 'metabolitelist'
  }, {
    ref: 'metaboliteAddForm', selector: 'metaboliteaddform'
  }, {
    ref: 'metabolitePanel', selector: 'metabolitepanel'
  }],
  init: function() {
    console.log('Metabolites controller init');
    var me = this;

    // configure store
    var store = this.getMetabolitesStore();
    store.pageSize = this.application.getPageSize();
    store.setUrl(this.application.metabolitesUrl('json'));
    store.on('load', this.onLoad, this);
    store.on('beforeload', this.onBeforeLoad, this);

    this.control({
      'metabolitelist': {
        select: this.onSelect,
        deselect: this.onDeselect,
        beforeselect: this.beforeSelect,
        metabolize: this.showMetabolizeStructureForm
      },
      'metabolitelist component[action=pagesize]': {
        select: this.onPageSizeChange
      },
      'metabolitepanel component[action=download]': {
        click: this.showDownloadMenu
      },
      'metabolitepanel component[action=actions]': {
        click: this.showActionsMenu
      },
      'metaboliteaddform component[action=addstructures]': {
        click: this.addStructuresHandler
      },
      'metaboliteaddform component[action=addstructurescancel]': {
        click: this.showGrid
      },
      'metabolizeform component[action=metabolize]': {
        click: this.metabolizeHandler
      },
      'metabolizeoneform component[action=metabolize_one]': {
        click: this.metabolizeOneHandler
      }
    });

    this.application.on('selectscan', this.applyScanFilter, this);
    this.application.on('noselectscan', this.clearScanFilter, this);

    /**
     * @property {Boolean} hasMSData
     * Whether there is ms data to use for annotate
     * Used in action forms to disable/enable annotate options
     */
    this.hasMSData = false;
    this.application.on('chromatogramload', this.onChromatrogramLoad, this);
    this.application.on('rpcsubmitsuccess', function() {
      Ext.getCmp('addstructuresaction').disable();
      Ext.getCmp('metabolizeaction').disable();
      me.getMetaboliteList().getCommandsColumn().disableAction();
    });

    this.application.addEvents(
        /**
         * @event
         * Triggered when metabolite store is loaded.
         * @param {Ext.data.Store} store
         */
        'metaboliteload',
        /**
         * @event
         * Triggered when metabolite is selected.
         * @param {Number} metid Metabolite identifier
         * @param {Esc.magmaweb.model.Metabolite} metabolite
         */
        'metaboliteselect',
        /**
         * @event
         * Triggered when metabolite is deselected.
         * @param {Number} metid Metabolite identifier
         * @param {Esc.magmaweb.model.Metabolite} metabolite
         */
        'metabolitedeselect',
        /**
         * @event
         * Triggered when metabolite selection is cleared
         */
        'metabolitenoselect'
    );

    this.actionsMenu = Ext.create('Ext.menu.Menu', {
        items: [{
            iconCls: 'icon-add',
            id: 'addstructuresaction',
            text: 'Add structures',
            handler: this.showAddStructuresForm.bind(this)
        }, {
            text: 'Metabolize',
            id: 'metabolizeaction',
            tooltip: 'Metabolize all structures',
            disabled: true,
            handler: this.showMetabolizeForm.bind(this)
        }, {
            text: 'Clear filters',
            handler: this.clearFilters.bind(this)
        }]
    });

    this.downloadMenu = Ext.create('Ext.menu.Menu', {
        items: [{
            text: 'CSV',
            tooltip: 'Save metabolites as comma seperated file',
            handler: this.download_csv.bind(this)
        }, {
            text: 'SDF',
            tooltip: 'Save metabolites as sdf',
            handler: this.download_sdf.bind(this)
        }]
    });

    this.application.on('assignmentchanged', function(isAssigned, params) {
        me.getMetabolitesStore().load();
    });
  },
  /**
   * Loads metabolite store
   */
  onLaunch: function() {
      // store not loaded in init because metaboliteload event is fired before listeners of views are registerd
      // the nhits column has an active filter
      // so do not use list.store.load() , but trigger a filter update to load
      this.getMetaboliteList().filters.createFilters();
      this.getMetabolitesStore().load();
      this.getMetaboliteList().setPageSize(this.getMetabolitesStore().pageSize);
      this.applyRole();
  },
  /**
   * Listens for metabolite store load event.
   * Selects metabolite if store only contains 1 metabolite.
   *
   * @param {Ext.data.Store} store
   */
  onLoad: function(store) {
    this.application.fireEvent('metaboliteload', store);
    this.metabolizable(store.getTotalUnfilteredCount() > 0);
    if (store.getCount() == 1 && !this.getSelectionModel().hasSelection()) {
        console.log('Only one metabolite loaded and its not selected, selecting it');
        this.getSelectionModel().select(0);
    }
    if (store.getTotalUnfilteredCount() === 0) {
        this.showAddStructuresForm();
    }
  },
  /**
   * If metabolite is selected then try to reselect it after load
   * If it fails to reselect it fires a metabolitedeselect event.
   */
  onBeforeLoad: function(store) {
      var me = this;
      var sm = me.getSelectionModel();
      if (sm && sm.hasSelection()) {
          var selected = sm.getSelection()[0].getId();

          var reselect = function() {
              var record = store.getById(selected);
              if (record !== null) {
                sm.select(record);
              } else {
                this.application.fireEvent('metabolitedeselect', selected, 'not found');
              }
              store.removeListener('load', reselect, me);
          };

          store.on('load', reselect , me);
      }
  },
  /**
   * Fetch selection model of metabolite list.
   * @return {Ext.selection.Model}
   */
  getSelectionModel: function() {
    return this.getMetaboliteList().getSelectionModel();
  },
  /**
   * Listens for chromatogram load event.
   * And toggles annotation fieldset in add structures form.
   * @param {Esc.d3.Chromatogram} chromatogram
   */
  onChromatrogramLoad: function(chromatogram) {
    this.hasMSData = chromatogram.data.length > 0;
    this.getMetaboliteAddForm().setDisabledAnnotateFieldset(!this.hasMSData);
  },
  /**
   * Only allow metabolite with a scans to be selected.
   * The extracted ion chromatogram of a metabolite without scans can not be shown because it can not be selected.
   */
  beforeSelect: function(rm, metabolite) {
    return (metabolite.data.nhits > 0);
  },
  onSelect: function(rm, metabolite) {
    var metid = metabolite.data.metid;
    this.application.fireEvent('metaboliteselect', metid, metabolite);
  },
  onDeselect: function(rm, metabolite) {
    var metid = metabolite.data.metid;
    this.application.fireEvent('metabolitedeselect', metid, metabolite);
  },
  /**
   * Remove filters and clears selection
   */
  clearFilters: function() {
    console.log('Clear metabolite filters');
    this.getMetaboliteList().clearFilters();
    this.application.fireEvent('metabolitenoselect');
  },
  /**
   * Apply scan filter to metabolite store.
   * And shows fragment score column.
   *
   * @param {Number} scanid Scan identifier to filter on.
   */
  applyScanFilter: function(scanid) {
      this.getMetabolitesStore().setScanFilter(scanid);
      this.getMetaboliteList().showFragmentScoreColumn();
      // TODO in spectra add markers for metabolites present in scan
  },
  /**
   * Removes scan filter from metabolite store.
   * And hides fragment score/deltappm column.
   * And deactivates filters on fragment score/deltappm column if any
   * And resets sort if store is sorted on fragment score/deltappm.
   */
  clearScanFilter: function() {
      var store = this.getMetabolitesStore();
      if ('filters' in store) {
          store.filters.removeAtKey('score');
          store.filters.removeAtKey('deltappm');
      }
      if ('sorters' in store) {
          store.sorters.removeAtKey('score');
          store.sorters.removeAtKey('deltappm');
      }
      store.removeScanFilter();
      this.getMetaboliteList().hideFragmentScoreColumn();
  },
  onPageSizeChange: function(combo) {
      this.getMetabolitesStore().setPageSize(combo.getValue());
  },
  /**
   * Open a new window with metabolites as comma seperated file or sdf.
   * Uses store/proxy/gridfilter state to construct queryString so what you see in grid is what in csv file.
   *
   * @params {String} format Can be csv or sdf.
   */
  download: function(format) {
    // download needs to make an url with has the same query parameters as the store.load()
    // for load() the store builds an operation object, we need to build this aswell
    var store = this.getMetabolitesStore();
    var proxy = store.getProxy();
    var config = {
        action: 'read',
        groupers: store.groupers.items,
        limit: store.pageSize,
        page: store.currentPage,
        start: (store.currentPage-1)*store.pageSize,
        sorters: store.getSorters()
    };
    var operation = Ext.create('Ext.data.Operation', config);
    var request = proxy.buildRequest(operation);
    var params = request.params;
    // Ext.ux.grid.FiltersFeature adds filters to request.params in store.beforeLoad event handler
    // so we do the same to get the filter query string
    Ext.apply(params, this.getMetaboliteList().getFilterQuery());

    // visible ordered columns
    params['cols'] = Ext.JSON.encode(this.getMetaboliteList().getVisiblColumnIndices());

    var url = Ext.urlAppend(
        this.application.metabolitesUrl(format),
        Ext.Object.toQueryString(params)
    );
    window.open(url, 'metabolites'+format);
  },
  /**
   * Shortcut do download csv file
   */
  download_csv: function() {
    this.download('csv');
  },
  /**
   * Shortcut do download sdf file
   */
  download_sdf: function() {
    this.download('sdf');
  },
  /**
   * Loads defaults of add structures form and
   * Shows add structures form in metabolites panel.
   */
  showAddStructuresForm: function() {
      this.getMetaboliteAddForm().loadDefaults(this.application.runInfoUrl());
      this.getMetabolitePanel().setActiveItem(1);
  },
  /**
   * Shows list or grid in metabolite panel.
   */
  showGrid: function() {
      this.getMetabolitePanel().setActiveItem(0);
  },
  /**
   * Handler for submit button in Add structures form.
   *
   */
  addStructuresHandler: function() {
    var me = this;
    var form = this.getMetaboliteAddForm().getForm();
    if (form.isValid()) {
      form.submit({
        url: me.application.rpcUrl('add_structures'),
        waitMsg: 'Submitting action ...',
        submitEmptyText: false,
        success: function(fp, o) {
          var response = Ext.JSON.decode(o.response.responseText);
          me.application.fireEvent('rpcsubmitsuccess', response.jobid);
          // switch back to grid so results can still be used while job is running
          me.showGrid();
        },
        failure: function(form, action) {
          if (action.failureType === "server") {
            Ext.Error.raise(Ext.JSON.decode(action.response.responseText));
          } else {
            Ext.Error.raise(action.response.responseText);
          }
        }
      });
    }
  },
  /**
   * Shows metabolize form in modal window
   */
  showMetabolizeForm: function() {
    var me = this;
    if (!this.metabolizeForm) {
        this.metabolizeForm = Ext.create('Esc.magmaweb.view.metabolite.MetabolizeForm');
        this.metabolizeForm.loadDefaults(me.application.runInfoUrl());
    }
    this.metabolizeForm.setDisabledAnnotateFieldset(!this.hasMSData);
    this.metabolizeForm.show();
  },
  metabolizeHandler: function(button) {
    var me = this;
    var wf = this.metabolizeForm;
    var form = wf.getForm();
    if (form.isValid()) {
      form.submit({
        url : me.application.rpcUrl('metabolize'),
        waitMsg: 'Submitting action ...',
        submitEmptyText: false,
        success: function(fp, o) {
          var response = Ext.JSON.decode(o.response.responseText);
          me.application.fireEvent('rpcsubmitsuccess', response.jobid);
          wf.hide();
        },
        failure: function(form, action) {
          wf.hide();
          if (action.failureType === "server") {
            Ext.Error.raise(Ext.JSON.decode(action.response.responseText));
          } else {
            Ext.Error.raise(action.response.responseText);
          }
        }
      });
    }
  },
  /**
   * Shows metabolize form in modal window for one metabolite/structure
   * @param {Ext.data.Model} rec Record to metabolize
   */
  showMetabolizeStructureForm: function(rec) {
    var me = this;
    if (!this.metabolizeStructureForm) {
        this.metabolizeStructureForm = Ext.create('Esc.magmaweb.view.metabolite.MetabolizeOneForm');
        this.metabolizeStructureForm.loadDefaults(me.application.runInfoUrl());
    }
    this.metabolizeStructureForm.setMetabolite(rec);
    this.metabolizeStructureForm.setDisabledAnnotateFieldset(!this.hasMSData);
    this.metabolizeStructureForm.show();
  },
  /**
   * Handler for submit button for metabolize one structure form.
   */
  metabolizeOneHandler: function() {
    var me = this;
    var wf = this.metabolizeStructureForm;
    var form = wf.getForm();
    if (form.isValid()) {
      form.submit({
        url : me.application.rpcUrl('metabolize_one'),
        waitMsg: 'Submitting action ...',
        submitEmptyText: false,
        success: function(fp, o) {
          var response = Ext.JSON.decode(o.response.responseText);
          me.application.fireEvent('rpcsubmitsuccess', response.jobid);
          wf.hide();
        },
        failure: function(form, action) {
          wf.hide();
          if (action.failureType === "server") {
            Ext.Error.raise(Ext.JSON.decode(action.response.responseText));
          } else {
            Ext.Error.raise(action.response.responseText);
          }
        }
      });
    }
  },
  /**
   * Enable or disable metabolize action.
   *
   * @param {Boolean} enabled True is metabolizable
   */
  metabolizable: function(enabled) {
     Ext.getCmp('metabolizeaction').setDisabled(!enabled);
  },
  /**
   * Show actions menu at event xy
   * @param {Ext.Element} tool
   * @param {Ext.EventObject} event
   */
  showActionsMenu: function(tool, event) {
     this.actionsMenu.showAt(event.getXY());
  },
  /**
   * Show download menu at event xy
   * @param {Ext.Element} tool
   * @param {Ext.EventObject} event
   */
  showDownloadMenu: function(tool, event) {
    this.downloadMenu.showAt(event.getXY());
  },
  /**
   * Apply role to user interface.
   * Checks canRun and if false removes all action buttons.
   */
  applyRole: function() {
	  if (this.application.canRun) {
		  return;
	  }
	  this.actionsMenu.getComponent('addstructuresaction').hide();
	  this.actionsMenu.getComponent('metabolizeaction').hide();
	  this.getMetaboliteList().hideCommandsColumn();
  }
});

<commit_msg>In the molecules controller the wrong name was used for the molecules per page combo (fixes #59).<commit_after>/**
 * Metabolite controller.
 *
 * Handles actions performed in metabolites views.
 * @author Stefan Verhoeven
 */
Ext.define('Esc.magmaweb.controller.Metabolites', {
  extend: 'Ext.app.Controller',
  views: [ 'metabolite.Panel' ],
  stores: [ 'Metabolites' ],
  models: [ 'Metabolite' ],
  uses: [
    'Esc.magmaweb.view.metabolite.MetabolizeForm',
    'Esc.magmaweb.view.metabolite.MetabolizeOneForm'
  ],
  refs: [{
    ref: 'metaboliteList', selector: 'metabolitelist'
  }, {
    ref: 'metaboliteAddForm', selector: 'metaboliteaddform'
  }, {
    ref: 'metabolitePanel', selector: 'metabolitepanel'
  }],
  init: function() {
    console.log('Metabolites controller init');
    var me = this;

    // configure store
    var store = this.getMetabolitesStore();
    store.pageSize = this.application.getPageSize();
    store.setUrl(this.application.metabolitesUrl('json'));
    store.on('load', this.onLoad, this);
    store.on('beforeload', this.onBeforeLoad, this);

    this.control({
      'metabolitelist': {
        select: this.onSelect,
        deselect: this.onDeselect,
        beforeselect: this.beforeSelect,
        metabolize: this.showMetabolizeStructureForm
      },
      'metabolitelist component[action=pagesizeCombo]': {
        select: this.onPageSizeChange
      },
      'metabolitepanel component[action=download]': {
        click: this.showDownloadMenu
      },
      'metabolitepanel component[action=actions]': {
        click: this.showActionsMenu
      },
      'metaboliteaddform component[action=addstructures]': {
        click: this.addStructuresHandler
      },
      'metaboliteaddform component[action=addstructurescancel]': {
        click: this.showGrid
      },
      'metabolizeform component[action=metabolize]': {
        click: this.metabolizeHandler
      },
      'metabolizeoneform component[action=metabolize_one]': {
        click: this.metabolizeOneHandler
      }
    });

    this.application.on('selectscan', this.applyScanFilter, this);
    this.application.on('noselectscan', this.clearScanFilter, this);

    /**
     * @property {Boolean} hasMSData
     * Whether there is ms data to use for annotate
     * Used in action forms to disable/enable annotate options
     */
    this.hasMSData = false;
    this.application.on('chromatogramload', this.onChromatrogramLoad, this);
    this.application.on('rpcsubmitsuccess', function() {
      Ext.getCmp('addstructuresaction').disable();
      Ext.getCmp('metabolizeaction').disable();
      me.getMetaboliteList().getCommandsColumn().disableAction();
    });

    this.application.addEvents(
        /**
         * @event
         * Triggered when metabolite store is loaded.
         * @param {Ext.data.Store} store
         */
        'metaboliteload',
        /**
         * @event
         * Triggered when metabolite is selected.
         * @param {Number} metid Metabolite identifier
         * @param {Esc.magmaweb.model.Metabolite} metabolite
         */
        'metaboliteselect',
        /**
         * @event
         * Triggered when metabolite is deselected.
         * @param {Number} metid Metabolite identifier
         * @param {Esc.magmaweb.model.Metabolite} metabolite
         */
        'metabolitedeselect',
        /**
         * @event
         * Triggered when metabolite selection is cleared
         */
        'metabolitenoselect'
    );

    this.actionsMenu = Ext.create('Ext.menu.Menu', {
        items: [{
            iconCls: 'icon-add',
            id: 'addstructuresaction',
            text: 'Add structures',
            handler: this.showAddStructuresForm.bind(this)
        }, {
            text: 'Metabolize',
            id: 'metabolizeaction',
            tooltip: 'Metabolize all structures',
            disabled: true,
            handler: this.showMetabolizeForm.bind(this)
        }, {
            text: 'Clear filters',
            handler: this.clearFilters.bind(this)
        }]
    });

    this.downloadMenu = Ext.create('Ext.menu.Menu', {
        items: [{
            text: 'CSV',
            tooltip: 'Save metabolites as comma seperated file',
            handler: this.download_csv.bind(this)
        }, {
            text: 'SDF',
            tooltip: 'Save metabolites as sdf',
            handler: this.download_sdf.bind(this)
        }]
    });

    this.application.on('assignmentchanged', function(isAssigned, params) {
        me.getMetabolitesStore().load();
    });
  },
  /**
   * Loads metabolite store
   */
  onLaunch: function() {
      // store not loaded in init because metaboliteload event is fired before listeners of views are registerd
      // the nhits column has an active filter
      // so do not use list.store.load() , but trigger a filter update to load
      this.getMetaboliteList().filters.createFilters();
      this.getMetabolitesStore().load();
      this.getMetaboliteList().setPageSize(this.getMetabolitesStore().pageSize);
      this.applyRole();
  },
  /**
   * Listens for metabolite store load event.
   * Selects metabolite if store only contains 1 metabolite.
   *
   * @param {Ext.data.Store} store
   */
  onLoad: function(store) {
    this.application.fireEvent('metaboliteload', store);
    this.metabolizable(store.getTotalUnfilteredCount() > 0);
    if (store.getCount() == 1 && !this.getSelectionModel().hasSelection()) {
        console.log('Only one metabolite loaded and its not selected, selecting it');
        this.getSelectionModel().select(0);
    }
    if (store.getTotalUnfilteredCount() === 0) {
        this.showAddStructuresForm();
    }
  },
  /**
   * If metabolite is selected then try to reselect it after load
   * If it fails to reselect it fires a metabolitedeselect event.
   */
  onBeforeLoad: function(store) {
      var me = this;
      var sm = me.getSelectionModel();
      if (sm && sm.hasSelection()) {
          var selected = sm.getSelection()[0].getId();

          var reselect = function() {
              var record = store.getById(selected);
              if (record !== null) {
                sm.select(record);
              } else {
                this.application.fireEvent('metabolitedeselect', selected, 'not found');
              }
              store.removeListener('load', reselect, me);
          };

          store.on('load', reselect , me);
      }
  },
  /**
   * Fetch selection model of metabolite list.
   * @return {Ext.selection.Model}
   */
  getSelectionModel: function() {
    return this.getMetaboliteList().getSelectionModel();
  },
  /**
   * Listens for chromatogram load event.
   * And toggles annotation fieldset in add structures form.
   * @param {Esc.d3.Chromatogram} chromatogram
   */
  onChromatrogramLoad: function(chromatogram) {
    this.hasMSData = chromatogram.data.length > 0;
    this.getMetaboliteAddForm().setDisabledAnnotateFieldset(!this.hasMSData);
  },
  /**
   * Only allow metabolite with a scans to be selected.
   * The extracted ion chromatogram of a metabolite without scans can not be shown because it can not be selected.
   */
  beforeSelect: function(rm, metabolite) {
    return (metabolite.data.nhits > 0);
  },
  onSelect: function(rm, metabolite) {
    var metid = metabolite.data.metid;
    this.application.fireEvent('metaboliteselect', metid, metabolite);
  },
  onDeselect: function(rm, metabolite) {
    var metid = metabolite.data.metid;
    this.application.fireEvent('metabolitedeselect', metid, metabolite);
  },
  /**
   * Remove filters and clears selection
   */
  clearFilters: function() {
    console.log('Clear metabolite filters');
    this.getMetaboliteList().clearFilters();
    this.application.fireEvent('metabolitenoselect');
  },
  /**
   * Apply scan filter to metabolite store.
   * And shows fragment score column.
   *
   * @param {Number} scanid Scan identifier to filter on.
   */
  applyScanFilter: function(scanid) {
      this.getMetabolitesStore().setScanFilter(scanid);
      this.getMetaboliteList().showFragmentScoreColumn();
      // TODO in spectra add markers for metabolites present in scan
  },
  /**
   * Removes scan filter from metabolite store.
   * And hides fragment score/deltappm column.
   * And deactivates filters on fragment score/deltappm column if any
   * And resets sort if store is sorted on fragment score/deltappm.
   */
  clearScanFilter: function() {
      var store = this.getMetabolitesStore();
      if ('filters' in store) {
          store.filters.removeAtKey('score');
          store.filters.removeAtKey('deltappm');
      }
      if ('sorters' in store) {
          store.sorters.removeAtKey('score');
          store.sorters.removeAtKey('deltappm');
      }
      store.removeScanFilter();
      this.getMetaboliteList().hideFragmentScoreColumn();
  },
  onPageSizeChange: function(combo) {
      this.getMetabolitesStore().setPageSize(combo.getValue());
  },
  /**
   * Open a new window with metabolites as comma seperated file or sdf.
   * Uses store/proxy/gridfilter state to construct queryString so what you see in grid is what in csv file.
   *
   * @params {String} format Can be csv or sdf.
   */
  download: function(format) {
    // download needs to make an url with has the same query parameters as the store.load()
    // for load() the store builds an operation object, we need to build this aswell
    var store = this.getMetabolitesStore();
    var proxy = store.getProxy();
    var config = {
        action: 'read',
        groupers: store.groupers.items,
        limit: store.pageSize,
        page: store.currentPage,
        start: (store.currentPage-1)*store.pageSize,
        sorters: store.getSorters()
    };
    var operation = Ext.create('Ext.data.Operation', config);
    var request = proxy.buildRequest(operation);
    var params = request.params;
    // Ext.ux.grid.FiltersFeature adds filters to request.params in store.beforeLoad event handler
    // so we do the same to get the filter query string
    Ext.apply(params, this.getMetaboliteList().getFilterQuery());

    // visible ordered columns
    params['cols'] = Ext.JSON.encode(this.getMetaboliteList().getVisiblColumnIndices());

    var url = Ext.urlAppend(
        this.application.metabolitesUrl(format),
        Ext.Object.toQueryString(params)
    );
    window.open(url, 'metabolites'+format);
  },
  /**
   * Shortcut do download csv file
   */
  download_csv: function() {
    this.download('csv');
  },
  /**
   * Shortcut do download sdf file
   */
  download_sdf: function() {
    this.download('sdf');
  },
  /**
   * Loads defaults of add structures form and
   * Shows add structures form in metabolites panel.
   */
  showAddStructuresForm: function() {
      this.getMetaboliteAddForm().loadDefaults(this.application.runInfoUrl());
      this.getMetabolitePanel().setActiveItem(1);
  },
  /**
   * Shows list or grid in metabolite panel.
   */
  showGrid: function() {
      this.getMetabolitePanel().setActiveItem(0);
  },
  /**
   * Handler for submit button in Add structures form.
   *
   */
  addStructuresHandler: function() {
    var me = this;
    var form = this.getMetaboliteAddForm().getForm();
    if (form.isValid()) {
      form.submit({
        url: me.application.rpcUrl('add_structures'),
        waitMsg: 'Submitting action ...',
        submitEmptyText: false,
        success: function(fp, o) {
          var response = Ext.JSON.decode(o.response.responseText);
          me.application.fireEvent('rpcsubmitsuccess', response.jobid);
          // switch back to grid so results can still be used while job is running
          me.showGrid();
        },
        failure: function(form, action) {
          if (action.failureType === "server") {
            Ext.Error.raise(Ext.JSON.decode(action.response.responseText));
          } else {
            Ext.Error.raise(action.response.responseText);
          }
        }
      });
    }
  },
  /**
   * Shows metabolize form in modal window
   */
  showMetabolizeForm: function() {
    var me = this;
    if (!this.metabolizeForm) {
        this.metabolizeForm = Ext.create('Esc.magmaweb.view.metabolite.MetabolizeForm');
        this.metabolizeForm.loadDefaults(me.application.runInfoUrl());
    }
    this.metabolizeForm.setDisabledAnnotateFieldset(!this.hasMSData);
    this.metabolizeForm.show();
  },
  metabolizeHandler: function(button) {
    var me = this;
    var wf = this.metabolizeForm;
    var form = wf.getForm();
    if (form.isValid()) {
      form.submit({
        url : me.application.rpcUrl('metabolize'),
        waitMsg: 'Submitting action ...',
        submitEmptyText: false,
        success: function(fp, o) {
          var response = Ext.JSON.decode(o.response.responseText);
          me.application.fireEvent('rpcsubmitsuccess', response.jobid);
          wf.hide();
        },
        failure: function(form, action) {
          wf.hide();
          if (action.failureType === "server") {
            Ext.Error.raise(Ext.JSON.decode(action.response.responseText));
          } else {
            Ext.Error.raise(action.response.responseText);
          }
        }
      });
    }
  },
  /**
   * Shows metabolize form in modal window for one metabolite/structure
   * @param {Ext.data.Model} rec Record to metabolize
   */
  showMetabolizeStructureForm: function(rec) {
    var me = this;
    if (!this.metabolizeStructureForm) {
        this.metabolizeStructureForm = Ext.create('Esc.magmaweb.view.metabolite.MetabolizeOneForm');
        this.metabolizeStructureForm.loadDefaults(me.application.runInfoUrl());
    }
    this.metabolizeStructureForm.setMetabolite(rec);
    this.metabolizeStructureForm.setDisabledAnnotateFieldset(!this.hasMSData);
    this.metabolizeStructureForm.show();
  },
  /**
   * Handler for submit button for metabolize one structure form.
   */
  metabolizeOneHandler: function() {
    var me = this;
    var wf = this.metabolizeStructureForm;
    var form = wf.getForm();
    if (form.isValid()) {
      form.submit({
        url : me.application.rpcUrl('metabolize_one'),
        waitMsg: 'Submitting action ...',
        submitEmptyText: false,
        success: function(fp, o) {
          var response = Ext.JSON.decode(o.response.responseText);
          me.application.fireEvent('rpcsubmitsuccess', response.jobid);
          wf.hide();
        },
        failure: function(form, action) {
          wf.hide();
          if (action.failureType === "server") {
            Ext.Error.raise(Ext.JSON.decode(action.response.responseText));
          } else {
            Ext.Error.raise(action.response.responseText);
          }
        }
      });
    }
  },
  /**
   * Enable or disable metabolize action.
   *
   * @param {Boolean} enabled True is metabolizable
   */
  metabolizable: function(enabled) {
     Ext.getCmp('metabolizeaction').setDisabled(!enabled);
  },
  /**
   * Show actions menu at event xy
   * @param {Ext.Element} tool
   * @param {Ext.EventObject} event
   */
  showActionsMenu: function(tool, event) {
     this.actionsMenu.showAt(event.getXY());
  },
  /**
   * Show download menu at event xy
   * @param {Ext.Element} tool
   * @param {Ext.EventObject} event
   */
  showDownloadMenu: function(tool, event) {
    this.downloadMenu.showAt(event.getXY());
  },
  /**
   * Apply role to user interface.
   * Checks canRun and if false removes all action buttons.
   */
  applyRole: function() {
	  if (this.application.canRun) {
		  return;
	  }
	  this.actionsMenu.getComponent('addstructuresaction').hide();
	  this.actionsMenu.getComponent('metabolizeaction').hide();
	  this.getMetaboliteList().hideCommandsColumn();
  }
});

<|endoftext|>


<commit_before>// Template.createSampleGroup

Template.createSampleGroup.onCreated(function() {
  let instance = this;

  instance.newSampleGroup = new ReactiveVar();
  instance.error = new ReactiveVar();
});

Template.createSampleGroup.helpers({
  nameAndDescription() {
    return SampleGroups.simpleSchema().pick(["name", "description"]);
  },
  error() { return Template.instance().error },
  newSampleGroup() { return Template.instance().newSampleGroup },
});

Template.createSampleGroup.events({
  "click .create-sample-group"(event, instance) {
    let sampleGroup = instance.newSampleGroup.get();

    Meteor.call("createSampleGroup", sampleGroup, (error, selected) => {
      if (error) {
        if (error.reason === "Match failed") {
          // there might be edge cases here which I haven't found yet so other
          // messages might have to be shown instead
          instance.error.set({ header: "Please correct errors above" });
        } else if (!error.details) {
          console.log("error:", error);
          instance.error.set({
            header: "Internal server error",
            message: error.reason || "Error: " + error.error,
          });
        } else {
          console.log("error:", error);
          instance.error.set({
            header: error.reason,
            message: error.details
          });
        }
      } else {
        FlowRouter.setParams({ selected });
      }
    });
  },
});

// Template.showSampleGroup

Template.showSampleGroup.helpers({
  slugToString: MedBook.utility.slugToString,
  downloadUrl() {
    let userId = Meteor.userId();
    let loginToken = Accounts._storedLoginToken();

    return `/download/${userId}/${loginToken}/data-collection/` +
        `SampleGroups/${this._id}`;
  },
  totalSampleCount() {
    return _.reduce(this.data_sets, (memo, sgDataSet) => {
      return memo + sgDataSet.sample_count;
    }, 0);
  },
});

// Sample Group Expression Level & Variance Filters

// Setup the client-side Blobs2 collection
Blobs2 = new Mongo.Collection('blobs');

// Subscribe to associated jobs & blobs
Template.sampleGroupExprVarFilters.onCreated(function(){
  let instance = this;
  let sampleGroupId = instance.data._id ;
  // use Template.subscriptionsReady to know when these are available
  console.log("setting up..."); // XXX
  instance.subscribe("jobsOfType", "ApplyExprAndVarianceFilters");
  console.log("subscribing with id", sampleGroupId); // XXX
  instance.subscribe("blobsAssociatedWithObject", "SampleGroups", sampleGroupId);
});

Template.sampleGroupExprVarFilters.helpers({

  // if a filter has been applied, the download URL for the
  // filtered data; otherwise, 'false'
  urlForFilteredData(){
    let fileName = "sampleGroup_with_expr_filter_applied.tsv" // TODO make sure this is consistent with jobrunner
    let foundBlob = Blobs2.findOne({
      //associated object is the sample group id
      "associated_object.collection_name":"SampleGroups",
      "associated_object.mongo_id":this._id,
      "file_name": fileName,
    });

    console.log("do w have any RELEVANT blobs", foundBlob); // XXX

    if(!foundBlob){return false;}
    // Construct the URL a la downloadUrl above

    let userId = Meteor.userId();
    let loginToken = Accounts._storedLoginToken();
    let url = `/download/${userId}/${loginToken}/blob/` +
        `${foundBlob._id}/${fileName}`;

    console.log("made url", url); // XXX
    return url;
  },

  // if there is a job currently processing or waiting, return its status;
  // else return false
  isJobRunning(){
    return getFilterJobStatus(this._id);
  },
  // did the job error out
  didJobFail(){
    return ( getFilterJobStatus(this._id) === "error" );
  }
});
// helper for isJobRunning & didJobFail above
// heavily depends on there only ever being 1 job per sample group
// as it will pick an arbitrary one
function getFilterJobStatus(sampleGroupId){
    console.log("checking for jobs with", sampleGroupId); // XXX
    let currentJob = Jobs.findOne({
      '$and': [
        {name: "ApplyExprAndVarianceFilters"},
        {status: {$in: ["creating", "waiting", "running","error"]}},
        {'args.sample_group_id': sampleGroupId},
      ],
    },);
    console.log("found job", currentJob); // XXX
    if(currentJob){ return currentJob.status; } else { return false; }
}

Template.sampleGroupExprVarFilters.events({
  // initiate the expression & variance filter job
  "click .button.run-job": function(event, instance){
    let sampleGroupId = instance.data._id ;
    console.log("about to run the job for expr var filter", sampleGroupId); // XXX
    // TODO anything else before calling the method?
    Meteor.call("applyExprVarianceFilters", sampleGroupId, (error, result) => {
      if(error){
        // TODO recover? throw?
      } else {
        console.log("called meteor method and got result", result); // XXX
        // TODO handle result
      }
    });
  }
});

// Template.waitAndThenPermissionDenied

Template.waitAndThenPermissionDenied.onCreated(function() {
  let instance = this;

  instance.waitForTheServer = new ReactiveVar(true);

  instance.timeoutHandle = Meteor.setTimeout(() => {
    instance.waitForTheServer.set(false);
  }, 15 * 1000);
});

Template.waitAndThenPermissionDenied.events({
  "click .skip-wait"(event, instance) {
    Meteor.clearTimeout(instance.timeoutHandle);

    instance.waitForTheServer.set(false);
  },
});
<commit_msg>custom download filename for filtered samples<commit_after>// Template.createSampleGroup

Template.createSampleGroup.onCreated(function() {
  let instance = this;

  instance.newSampleGroup = new ReactiveVar();
  instance.error = new ReactiveVar();
});

Template.createSampleGroup.helpers({
  nameAndDescription() {
    return SampleGroups.simpleSchema().pick(["name", "description"]);
  },
  error() { return Template.instance().error },
  newSampleGroup() { return Template.instance().newSampleGroup },
});

Template.createSampleGroup.events({
  "click .create-sample-group"(event, instance) {
    let sampleGroup = instance.newSampleGroup.get();

    Meteor.call("createSampleGroup", sampleGroup, (error, selected) => {
      if (error) {
        if (error.reason === "Match failed") {
          // there might be edge cases here which I haven't found yet so other
          // messages might have to be shown instead
          instance.error.set({ header: "Please correct errors above" });
        } else if (!error.details) {
          console.log("error:", error);
          instance.error.set({
            header: "Internal server error",
            message: error.reason || "Error: " + error.error,
          });
        } else {
          console.log("error:", error);
          instance.error.set({
            header: error.reason,
            message: error.details
          });
        }
      } else {
        FlowRouter.setParams({ selected });
      }
    });
  },
});

// Template.showSampleGroup

Template.showSampleGroup.helpers({
  slugToString: MedBook.utility.slugToString,
  downloadUrl() {
    let userId = Meteor.userId();
    let loginToken = Accounts._storedLoginToken();

    return `/download/${userId}/${loginToken}/data-collection/` +
        `SampleGroups/${this._id}`;
  },
  totalSampleCount() {
    return _.reduce(this.data_sets, (memo, sgDataSet) => {
      return memo + sgDataSet.sample_count;
    }, 0);
  },
});

// Sample Group Expression Level & Variance Filters

// Setup the client-side Blobs2 collection
Blobs2 = new Mongo.Collection('blobs');

// Subscribe to associated jobs & blobs
Template.sampleGroupExprVarFilters.onCreated(function(){
  let instance = this;
  let sampleGroupId = instance.data._id ;
  // use Template.subscriptionsReady to know when these are available
  console.log("setting up..."); // XXX
  instance.subscribe("jobsOfType", "ApplyExprAndVarianceFilters");
  console.log("subscribing with id", sampleGroupId); // XXX
  instance.subscribe("blobsAssociatedWithObject", "SampleGroups", sampleGroupId);
});

Template.sampleGroupExprVarFilters.helpers({

  // if a filter has been applied, the download URL for the
  // filtered data; otherwise, 'false'
  urlForFilteredData(){
    let fileName = "sampleGroup_filteredByExprAndVar.tsv"
    let foundBlob = Blobs2.findOne({
      //associated object is the sample group id
      "associated_object.collection_name":"SampleGroups",
      "associated_object.mongo_id":this._id,
      "file_name": fileName,
    });

    console.log("do w have any RELEVANT blobs", foundBlob); // XXX

    if(!foundBlob){return false;}
    // Construct the URL a la downloadUrl above

    // Use the samplegroup name to make a custom download
    // filename. Url encode it since it will be going into a path
    let sampleGroupName = encodeURI(this.name);
    let visibleFileName = `${sampleGroupName}.genes_filtered.tsv`;
    let userId = Meteor.userId();
    let loginToken = Accounts._storedLoginToken();

    let url = `/download/${userId}/${loginToken}/blob/` +
        `${foundBlob._id}/${visibleFileName}`;

    console.log("made url", url); // XXX
    return url;
  },

  // if there is a job currently processing or waiting, return its status;
  // else return false
  isJobRunning(){
    return getFilterJobStatus(this._id);
  },
  // did the job error out
  didJobFail(){
    return ( getFilterJobStatus(this._id) === "error" );
  }
});
// helper for isJobRunning & didJobFail above
// heavily depends on there only ever being 1 job per sample group
// as it will pick an arbitrary one
function getFilterJobStatus(sampleGroupId){
    console.log("checking for jobs with", sampleGroupId); // XXX
    let currentJob = Jobs.findOne({
      '$and': [
        {name: "ApplyExprAndVarianceFilters"},
        {status: {$in: ["creating", "waiting", "running","error"]}},
        {'args.sample_group_id': sampleGroupId},
      ],
    },);
    console.log("found job", currentJob); // XXX
    if(currentJob){ return currentJob.status; } else { return false; }
}

Template.sampleGroupExprVarFilters.events({
  // initiate the expression & variance filter job
  "click .button.run-job": function(event, instance){
    let sampleGroupId = instance.data._id ;
    console.log("about to run the job for expr var filter", sampleGroupId); // XXX
    // TODO anything else before calling the method?
    Meteor.call("applyExprVarianceFilters", sampleGroupId, (error, result) => {
      if(error){
        // TODO recover? throw?
      } else {
        console.log("called meteor method and got result", result); // XXX
        // TODO handle result
      }
    });
  }
});

// Template.waitAndThenPermissionDenied

Template.waitAndThenPermissionDenied.onCreated(function() {
  let instance = this;

  instance.waitForTheServer = new ReactiveVar(true);

  instance.timeoutHandle = Meteor.setTimeout(() => {
    instance.waitForTheServer.set(false);
  }, 15 * 1000);
});

Template.waitAndThenPermissionDenied.events({
  "click .skip-wait"(event, instance) {
    Meteor.clearTimeout(instance.timeoutHandle);

    instance.waitForTheServer.set(false);
  },
});
<|endoftext|>


<commit_before>define([
    "sprd/model/ProductBase",
    'js/core/List',
    'js/data/AttributeTypeResolver',
    'sprd/entity/DesignConfiguration',
    'sprd/entity/TextConfiguration',
    'sprd/entity/Price',
    'js/data/TypeResolver', 'js/data/Entity', "underscore", "flow"],
    function (ProductBase, List, AttributeTypeResolver, DesignConfiguration, TextConfiguration, Price, TypeResolver, Entity, _, flow) {
        return ProductBase.inherit("sprd.model.Product", {

            schema: {
                configurations: [new AttributeTypeResolver({
                    attribute: "type",
                    mapping: {
                        "design": DesignConfiguration,
                        "text": TextConfiguration
                    }
                })]
            },

            defaults: {
                configurations: List
            },

            ctor: function () {
                this.callBase();

                var priceChangeHandler = function () {
                    this.trigger("priceChanged");
                };

                var productChangeHandler = function () {
                    this.trigger("productChanged");
                };

                this.bind("configurations", "add", priceChangeHandler, this);
                this.bind("configurations", "remove", priceChangeHandler, this);
                this.bind("configurations", "reset", priceChangeHandler, this);
                this.bind("configurations", "item:priceChanged", priceChangeHandler, this);

                this.bind('change:productType', productChangeHandler, this);
                this.bind('change:appearance', productChangeHandler, this);
                this.bind('configurations', 'add', productChangeHandler, this);
                this.bind('configurations', 'remove', productChangeHandler, this);
                this.bind('configurations', 'reset', productChangeHandler, this);
                this.bind('configurations', 'item:configurationChanged', productChangeHandler, this);
                this.bind('configurations', 'item:change:printArea', productChangeHandler, this);

            },

            price: function () {
                // TODO format price with currency
                if (this.$.price) {
                    return this.$.price;
                } else {
                    // calculate price
                    var price = new Price({
                        vatIncluded: this.get("productType.price.vatIncluded"),
                        currency: this.get('productType.price.currency')
                    });
                    this.$.configurations.each(function (configuration) {
                        price.add(configuration.price());
                    });

                    return price;
                }
            }.on("priceChanged", "change:productType"),

            _addConfiguration: function (configuration) {
                this.$.configurations.add(configuration);
            },

            getConfigurationsOnView: function (view) {

                view = view || this.$.view;

                var productType = this.$.productType;

                if (view && productType) {

                    if (productType.containsView(view)) {
                        return this.getConfigurationsOnPrintAreas(view.getPrintAreas());
                    } else {
                        throw new Error("View not on product type");
                    }

                }

                return [];

            },

            getConfigurationsOnPrintAreas: function (printAreas) {
                printAreas = printAreas || [];

                if (!(printAreas instanceof Array)) {
                    printAreas = [printAreas];
                }

                var ret = [];

                this.$.configurations.each(function (configuration) {
                    if (_.contains(printAreas, configuration.$.printArea)) {
                        ret.push(configuration);
                    }
                });

                return ret;

            },

            compose: function () {
                var ret = this.callBase();

                ret.restrictions = {
                    freeColorSelection: false,
                    example: false
                };

                var viewId = this.get("view.id");

                if (viewId) {
                    ret.defaultValues = {
                        defaultView: {
                            id: viewId
                        }
                    };
                }

                return ret;
            },

            fetch: function (options, callback) {
                var self = this;
                this.callBase(options, function (err) {
                    if (!err) {
                        self.$originalProduct = self.clone();
                    }
                    callback && callback(err, self);
                });
            },

            hasChanges: function() {
                return this.$originalProduct.isDeepEqual(this);
            },

            save: function (options, callback) {

                if (this.$originalProduct) {
                    if (this.hasChanges()) {
                        this.set('id', undefined);
                    } else {
                        callback && callback(null, this);
                        return;
                    }
                }

                var self = this;
                this.callBase(options, function (err) {
                    if (!err) {
                        self.$originalProduct = self.clone();
                    }

                    callback && callback(err, self);
                });
            },

            init: function (callback) {
                var self = this;

                flow()
                    .seq(function (cb) {
                        if (self.isNew()) {
                            cb();
                        } else {
                            self.fetch(null, cb);
                        }
                    })
                    .seq(function (cb) {
                        var productType = self.$.productType;
                        productType.fetch(null, cb);
                    })
                    .seq(function () {
                        var productType = self.$.productType;

                        self.set({
                            appearance: productType.getAppearanceById(self.$.appearance.$.id),
                            view: productType.getViewById(self.get("defaultValues.defaultView.id")) || productType.getDefaultView()
                        });
                    })
                    .seq(function (cb) {
                        flow()
                            .parEach(self.$.configurations.$items, function (configuration, cb) {
                                configuration.init(cb);
                            })
                            .exec(cb);
                    })
                    .exec(function (err) {
                        if (err) {
                            callback && callback(err);
                        } else {
                            callback && callback(null, self);
                        }
                    });
            },

            appearanceBrightness: function () {

                var color = this.get("appearance.getMainColor()");

                if (color) {
                    return color.distanceTo("#000000") < color.distanceTo("#FFFFFF") ?
                        "dark" : "bright";
                }

                return "";
            }.onChange(["appearance"])

        });
    });
<commit_msg>code style<commit_after>define(['sprd/model/ProductBase', 'js/core/List', 'js/data/AttributeTypeResolver', 'sprd/entity/DesignConfiguration', 'sprd/entity/TextConfiguration', 'sprd/entity/Price', 'js/data/TypeResolver', 'js/data/Entity', "underscore", "flow"], function (ProductBase, List, AttributeTypeResolver, DesignConfiguration, TextConfiguration, Price, TypeResolver, Entity, _, flow) {
    return ProductBase.inherit("sprd.model.Product", {

        schema: {
            configurations: [new AttributeTypeResolver({
                attribute: "type",
                mapping: {
                    "design": DesignConfiguration,
                    "text": TextConfiguration
                }
            })]
        },

        defaults: {
            configurations: List
        },

        ctor: function () {
            this.callBase();

            var priceChangeHandler = function () {
                this.trigger("priceChanged");
            };

            var productChangeHandler = function () {
                this.trigger("productChanged");
            };

            this.bind("configurations", "add", priceChangeHandler, this);
            this.bind("configurations", "remove", priceChangeHandler, this);
            this.bind("configurations", "reset", priceChangeHandler, this);
            this.bind("configurations", "item:priceChanged", priceChangeHandler, this);

            this.bind('change:productType', productChangeHandler, this);
            this.bind('change:appearance', productChangeHandler, this);
            this.bind('configurations', 'add', productChangeHandler, this);
            this.bind('configurations', 'remove', productChangeHandler, this);
            this.bind('configurations', 'reset', productChangeHandler, this);
            this.bind('configurations', 'item:configurationChanged', productChangeHandler, this);
            this.bind('configurations', 'item:change:printArea', productChangeHandler, this);

        },

        price: function () {
            // TODO format price with currency
            if (this.$.price) {
                return this.$.price;
            } else {
                // calculate price
                var price = new Price({
                    vatIncluded: this.get("productType.price.vatIncluded"),
                    currency: this.get('productType.price.currency')
                });
                this.$.configurations.each(function (configuration) {
                    price.add(configuration.price());
                });

                return price;
            }
        }.on("priceChanged", "change:productType"),

        _addConfiguration: function (configuration) {
            this.$.configurations.add(configuration);
        },

        getConfigurationsOnView: function (view) {

            view = view || this.$.view;

            var productType = this.$.productType;

            if (view && productType) {

                if (productType.containsView(view)) {
                    return this.getConfigurationsOnPrintAreas(view.getPrintAreas());
                } else {
                    throw new Error("View not on product type");
                }

            }

            return [];

        },

        getConfigurationsOnPrintAreas: function (printAreas) {
            printAreas = printAreas || [];

            if (!(printAreas instanceof Array)) {
                printAreas = [printAreas];
            }

            var ret = [];

            this.$.configurations.each(function (configuration) {
                if (_.contains(printAreas, configuration.$.printArea)) {
                    ret.push(configuration);
                }
            });

            return ret;

        },

        compose: function () {
            var ret = this.callBase();

            ret.restrictions = {
                freeColorSelection: false,
                example: false
            };

            var viewId = this.get("view.id");

            if (viewId) {
                ret.defaultValues = {
                    defaultView: {
                        id: viewId
                    }
                };
            }

            return ret;
        },

        fetch: function (options, callback) {
            var self = this;
            this.callBase(options, function (err) {
                if (!err) {
                    self.$originalProduct = self.clone();
                }
                callback && callback(err, self);
            });
        },

        hasChanges: function () {
            return this.$originalProduct.isDeepEqual(this);
        },

        save: function (options, callback) {

            if (this.$originalProduct) {
                if (this.hasChanges()) {
                    this.set('id', undefined);
                } else {
                    callback && callback(null, this);
                    return;
                }
            }

            var self = this;
            this.callBase(options, function (err) {
                if (!err) {
                    self.$originalProduct = self.clone();
                }

                callback && callback(err, self);
            });
        },

        init: function (callback) {
            var self = this;

            flow()
                .seq(function (cb) {
                    if (self.isNew()) {
                        cb();
                    } else {
                        self.fetch(null, cb);
                    }
                })
                .seq(function (cb) {
                    var productType = self.$.productType;
                    productType.fetch(null, cb);
                })
                .seq(function () {
                    var productType = self.$.productType;

                    self.set({
                        appearance: productType.getAppearanceById(self.$.appearance.$.id),
                        view: productType.getViewById(self.get("defaultValues.defaultView.id")) || productType.getDefaultView()
                    });
                })
                .seq(function (cb) {
                    flow()
                        .parEach(self.$.configurations.$items, function (configuration, cb) {
                            configuration.init(cb);
                        })
                        .exec(cb);
                })
                .exec(function (err) {
                    if (err) {
                        callback && callback(err);
                    } else {
                        callback && callback(null, self);
                    }
                });
        },

        appearanceBrightness: function () {

            var color = this.get("appearance.getMainColor()");

            if (color) {
                return color.distanceTo("#000000") < color.distanceTo("#FFFFFF") ?
                    "dark" : "bright";
            }

            return "";
        }.onChange(["appearance"])

    });
});
<|endoftext|>


<commit_before>"use strict";

var LWdb = require('./LWdb');

function BoxOfQuestions(db) {

        // private variables

        var _question = null; // no current question
        var _wordsToRepeat = null; // words which are eligible to be repeated.
                                   // initialisation to null forces calculation
                                   // on first call of wordsToRepeat()

         var _status = {};

        // private methods

        var _questionHasBeenProcessed = function(){

               _question = null;

               // This will trigger a new question, when LW.question()
               // is called the next time.

        };

        var _getRandomInt = function(min, max){
             // Returns a random integer between min (inclusive) and max (inclusive)
             // Using Math.round() will give you a non-uniform distribution!

    		return Math.floor(Math.random() * (max - min + 1)) + min;
	};

        var _shuffle =function(a) {
			 var j, x, i;
		    for (i = a.length; i; i--) {
		        j = Math.floor(Math.random() * i);
		        x = a[i - 1];
		        a[i - 1] = a[j];
		        a[j] = x;
		    };
                    return a
	};

        // ===============================================================================
        // literal object
        // ===============================================================================

        return {

        version: '0.2.1',

	db : db,

	name : db.dbName,

	chooseRandomObject : function(anArray){
                return anArray[_getRandomInt(0,anArray.length-1)];
	},

        question : function(){
            // gives back a question to ask
            if (!_question) {
                 // _question is null, go for a new one.
                 var wds = this.wordsToRepeat();
                 if (wds != null) {_question = this.chooseRandomObject(wds)}
            };
            return _question
        },

        answer :function(){
            return (this.question()).translate;
        },

       moveQuestionBackwards : function(){
            if (_question) { // we have a question

                // set new date for asking the question again;
                // this has to be a a delay period later.

                _question.date = new Date().valueOf() + (this.db.getSettings()).delay;

                // put the question back at the correct step

                var s = this.db.getSettings();

                if (s.offerLearnMode) { _question.step = 1;
                                       // step 0 is the learnmode, thus do not put
                                       // it at step 0
                                       // step 1 is the lowest step of the repeat mode.
                                      }
                else { // treat all the steps the same way, as repeat mode
                       // thus the lowest step value is 0
                       _question.step = 0;

                 }

                // An alternative which is not implemented:
                //
                // Set new step value to step - 1
                // With the result being not less than 1 or 0 depending on offerLearnMode.

                this.db.putWord(_question);

                // As the question has a new later date it is no more
                // a current question

                _questionHasBeenProcessed();
            }
        },

       moveQuestionForward : function(){

            if (_question) { // we have a question
                 var s = this.db.getSettings();

                // calculate new date. This depends on which step the question is.
                // And the delay calculation factor for that particular step.
                _question.date = new Date().valueOf() +
                                 s.delay * s.factorForDelayValue[_question.step];

                // With repeated calls to this method
                // the following will move the question up.
                //

                _question.step = _question.step + 1;

                // The assumption is that long delay values for higher steps
                // prevent an access error for
                //     s.factorForDelayValue[stepNumber]

                this.db.putWord(_question);

                // As the question has a new later date it is no more
                // a current question

                _questionHasBeenProcessed();

            }
       },

      importFrom : function(anArrayOfObjects){
       this.db.importFrom(anArrayOfObjects);
       },

       getAnswerOptions : function(numberOfOptions){
          // simple implementation : choose from all available words
          // As we use ECMA5script findIndex is not available.
          // We have to duplicate the effort in keeping an array of id
          // numbers called idsOfOptions and an array of objects called
          // options.

          var n = (db.getSettings()).numberOfOptions;

          var options = [];

          if (db.numberOfWords() >= n) {

             var q = this.question();
             options.push(q);

	     var idsOfOptions = [];
             idsOfOptions.push(q._id);

             var anOption;
             var allWords =  this.db.allWords();

             do {
                // choose option from all words.
                anOption = this.chooseRandomObject(allWords);

                if (idsOfOptions.indexOf(anOption._id) == -1) {
                        // the new option is not included yet
			idsOfOptions.push(anOption._id);
                        options.push(anOption)
               }

             } while (options.length < n);

          };
          return _shuffle(options)
       },

       config : function(config){
          throw new Error("not yet implemented");
       },

       status : function(){
         // give the number of words in the whole box
         // and the number of words in wordsToRepeat

         _status.numberOfWords = this.db.numberOfWords();

          // FIXME add more content to status

         return _status
       },

       wordsToRepeat : function(){

          var lowestStep = 0;  // all words with step value 0 and above are considered.
          var todayNow = new Date().valueOf();

          function isToBeRepeated(aWord) {
               return (aWord.step >= lowestStep) && (todayNow >= aWord.date);
          }

          if (_question == null || _wordsToRepeat == null ) {
                // _question == null means that either a question has never
                // been asked before or that a question has been asked and
                // processed but no new question yet has been picked.
                // In both cases a new _wordsToRepeat collection is necessary.

                _wordsToRepeat = (this.db.allWords()).filter(isToBeRepeated)
          };

          return _wordsToRepeat;
       },

       wordsWithStepValue : function(from, to){
          var toValue;

          if ( typeof(to) == "undefined" || to == null ) {toValue = from}
          else {toValue = to}

          function stepValueInRange(aWord) {
               return (aWord.step >= from) && (aWord.step <= toValue);
          }

          return (this.db.allWords()).filter(stepValueInRange);
       }

      }

}

module.exports = BoxOfQuestions;
<commit_msg>outline for session info<commit_after>"use strict";

var LWdb = require('./LWdb');

function BoxOfQuestions(db) {

        // private variables

        var _question = null; // no current question
        var _wordsToRepeat = null; // words which are eligible to be repeated.
                                   // initialisation to null forces calculation
                                   // on first call of wordsToRepeat()

         var _status = {};

        // private methods

        var _questionHasBeenProcessed = function(){

               _question = null;

               // This will trigger a new question, when LW.question()
               // is called the next time.

        };

        var _updateSessionInfo = function(){
               // FIXME
               // update session info

               // if (sessionStartDateProperty does not exist)
               // then create and set it. Set lastActivity property as well;
               // set "newSession" to true and return _status

               // if ((timeNow - lastActivity) > 30min)
               // then we have a new session ; set sessionStartDateProperty
               // set "newSession" to true and return _status

               // Set lastActivity property
               // set "newSession" to false and return _status

        };

        var _getRandomInt = function(min, max){
             // Returns a random integer between min (inclusive) and max (inclusive)
             // Using Math.round() will give you a non-uniform distribution!

    		return Math.floor(Math.random() * (max - min + 1)) + min;
	};

        var _shuffle =function(a) {
			 var j, x, i;
		    for (i = a.length; i; i--) {
		        j = Math.floor(Math.random() * i);
		        x = a[i - 1];
		        a[i - 1] = a[j];
		        a[j] = x;
		    };
                    return a
	};

        // ===============================================================================
        // literal object
        // ===============================================================================

        return {

        version: '0.2.1',

	db : db,

	name : db.dbName,

	chooseRandomObject : function(anArray){
                return anArray[_getRandomInt(0,anArray.length-1)];
	},

        question : function(){
            // gives back a question to ask
            if (!_question) {
                 // _question is null, go for a new one.
                 var wds = this.wordsToRepeat();
                 if (wds != null) {_question = this.chooseRandomObject(wds)}
            };
            return _question
        },

        answer :function(){
            return (this.question()).translate;
        },

       moveQuestionBackwards : function(){
            if (_question) { // we have a question

                // set new date for asking the question again;
                // this has to be a a delay period later.

                _question.date = new Date().valueOf() + (this.db.getSettings()).delay;

                // put the question back at the correct step

                var s = this.db.getSettings();
                // FIXME learnMode has a new interpretation
                if (s.offerLearnMode) { _question.step = 1;
                                       // step 0 is the learnmode, thus do not put
                                       // it at step 0
                                       // step 1 is the lowest step of the repeat mode.
                                      }
                else { // treat all the steps the same way, as repeat mode
                       // thus the lowest step value is 0
                       _question.step = 0;

                 }

                // An alternative which is not implemented:
                //
                // Set new step value to step - 1
                // With the result being not less than 1 or 0 depending on offerLearnMode.

                this.db.putWord(_question);

                // As the question has a new later date it is no more
                // a current question

                _questionHasBeenProcessed();
            }
        },

       moveQuestionForward : function(){

            if (_question) { // we have a question
                 var s = this.db.getSettings();

                // calculate new date. This depends on which step the question is.
                // And the delay calculation factor for that particular step.
                _question.date = new Date().valueOf() +
                                 s.delay * s.factorForDelayValue[_question.step];

                // With repeated calls to this method
                // the following will move the question up.
                //

                _question.step = _question.step + 1;

                // The assumption is that long delay values for higher steps
                // prevent an access error for
                //     s.factorForDelayValue[stepNumber]

                this.db.putWord(_question);

                // As the question has a new later date it is no more
                // a current question

                _questionHasBeenProcessed();

            }
       },

      importFrom : function(anArrayOfObjects){
       this.db.importFrom(anArrayOfObjects);
       },

       getAnswerOptions : function(numberOfOptions){
          // simple implementation : choose from all available words
          // As we use ECMA5script findIndex is not available.
          // We have to duplicate the effort in keeping an array of id
          // numbers called idsOfOptions and an array of objects called
          // options.

          var n = (db.getSettings()).numberOfOptions;

          var options = [];

          if (db.numberOfWords() >= n) {

             var q = this.question();
             options.push(q);

	     var idsOfOptions = [];
             idsOfOptions.push(q._id);

             var anOption;
             var allWords =  this.db.allWords();

             do {
                // choose option from all words.
                anOption = this.chooseRandomObject(allWords);

                if (idsOfOptions.indexOf(anOption._id) == -1) {
                        // the new option is not included yet
			idsOfOptions.push(anOption._id);
                        options.push(anOption)
               }

             } while (options.length < n);

          };
          return _shuffle(options)
       },

       config : function(config){
          throw new Error("not yet implemented");
       },

       status : function(){
         // give the number of words in the whole box
         // and the number of words in wordsToRepeat

         _status.numberOfWords = this.db.numberOfWords();

          // FIXME add more content to status

         return _status
       },

       wordsToRepeat : function(){

          var lowestStep = 0;  // all words with step value 0 and above are considered.
          var todayNow = new Date().valueOf();

          function isToBeRepeated(aWord) {
               return (aWord.step >= lowestStep) && (todayNow >= aWord.date);
          }

          if (_question == null || _wordsToRepeat == null ) {
                // _question == null means that either a question has never
                // been asked before or that a question has been asked and
                // processed but no new question yet has been picked.
                // In both cases a new _wordsToRepeat collection is necessary.

                _wordsToRepeat = (this.db.allWords()).filter(isToBeRepeated)

                _updateSessionInfo();

                // FIXME
                // check if we have a new session
                // if yes we need to check if we have enough _wordsToRepeat
                // if not we need to add some more addMoreWordsForLearning(n)
                // then recalculate _wordsToRepeat

          };

          return _wordsToRepeat;
       },

       addMoreWordsForLearning : function(n){
          // FIXME
          // update n words with step value < 0 to have a step value of 0
          var s = this.wordsWithStepValue(-10000,-1);
          // FIXME
          // s.sort according to step value descending, highest first.
          // set step value of the first n words to 0
       },

       wordsWithStepValue : function(from, to){
          var toValue;

          if ( typeof(to) == "undefined" || to == null ) {toValue = from}
          else {toValue = to}

          function stepValueInRange(aWord) {
               return (aWord.step >= from) && (aWord.step <= toValue);
          }

          return (this.db.allWords()).filter(stepValueInRange);
       }

      }

}

module.exports = BoxOfQuestions;
<|endoftext|>


<commit_before>/*
 * Specification for Element representing blocks in the proof:
 *
 * Regular proof blocks:
 *   - The cell has an attribute 'rule', which is the rule object of the logic.
 *   - Every SVG element that is an magnet (magnet: true) has an attribute 'port',
 *     that corresponds to the name of the port in the logic, e.g. "in2" or "hyp".
 * Assumption blocks:
 *   - The cell has an attribute 'assumption', with the number of the assumption it
 *     represents.
 * Conclusion blocks:
 *   - The cell has an attribute 'conclusion', with the number of the assumption it
 *     represents.
 *
 * Elements with an attribute 'prototypeElement' set to true are not part of the
 * proof, but rather the elements one can drag into the paper.
 *
 * Furthermore, every magnet has an attribute 'direction', which is either "in" or
 * "out". This is used by the UI to make sure wires are connected properly.
 *
 */

shapes = {};

/*
 * How does jointJS find the view for a model?
 * It takes the 'type' field, of the model, excepts it to be foo.bar
 * and then looks at join.shapes[foo][bar+'View']....
 */

joint.shapes.incredible = {}

/*
 * The new way of doing things: No more visual information in the model,
 * the shape is calculated in the view.
 * The whole crazy `attrs` feature of JointJS is not used!
 */
joint.shapes.incredible.Generic = joint.shapes.basic.Generic.extend({
  defaults: joint.util.deepSupplement({
    type: 'incredible.Generic',
  }, joint.shapes.basic.Generic.prototype.defaults)
});

joint.shapes.incredible.GenericView = joint.dia.ElementView.extend({
  initialize: function () {
    joint.dia.ElementView.prototype.initialize.apply(this, arguments);

    // Listen to some incredible-specific change events
    this.listenTo(this.model, 'change:brokenPorts', this.update);
    if (this.model.get('conclusion')) {
      this.listenTo(this.model, 'change:qed', this.update);
    };
  },

  renderMarkup: function() {
    // These will not all be defined
    var rule = this.model.get('rule');
    var assumption = this.model.get('assumption');
    var conclusion = this.model.get('conclusion');
    var task = this.model.get('task');

    var isPrototype = this.model.get('prototypeElement');

    if (rule) {
      var ports = rule.ports;
      var portsList = _.sortBy(_.map(ports, function (v, i) {return _.extend({id: i}, v);}), 'id');
      var portsGroup = _.groupBy(portsList, "type");
    } else if (assumption) {
      // Fake port data for assumption and conclusion blocks
      var portsGroup= {'conclusion': [{
        id: 'out'
      }]};
    } else if (conclusion) {
      var portsGroup= {'assumption': [{
        id: 'in'
      }]};
    }

    this.vel.attr('magnet',false);

    var group = V("<g/>");
    this.vel.append(group);

    var text = V("<text class='label' font-family='sans' fill='black'/>");
    if (rule) {
      text.text(rule.id);
    } else if (assumption) {
      text.text(task.assumptions[assumption-1]);
    } else if (conclusion) {
      text.text(task.conclusions[conclusion-1]);
    }

    group.append(text);
    textBB = text.bbox(true);
    // center text
    text.translate(- textBB.width/2, - textBB.height/2);

    var rect = V("<rect class='body' fill='#ecf0f1' rx='5' ry='5' stroke='#bdc3c7' stroke-opacity='0.5'/>");

    // Calculate minimum width/height based on number of ports and label length
    var height = 35;
    var width = 80;
    _.each(portsGroup, function (thesePorts, portType) {
      var total = _.size(thesePorts);
      if (portType == 'local hypothesis') {
        width = Math.max(width, 20 * total -5);
      } else {
        height = Math.max(height, 20 * total -5);
      }
    });
    width = Math.max(width, textBB.width + 10);
    height = Math.max(height, textBB.height + 10);

    rect.attr({width: width, height: height});
    rect.translate(-width/2,-height/2);
    group.prepend(rect);

    _.each(portsGroup, function (thesePorts, portType) {
      var total = _.size(thesePorts);
      _.each(thesePorts, function (portDesc, index) {
        var direction = ({assumption: 'in', conclusion: 'out', 'local hypothesis': 'out'})[portType];
          var pacman = V('<path class="port-body" stroke="none" fill="#777" magnet="true"/>');
          pacman.attr({port: portDesc.id, direction: direction});

          group.append(pacman);

          if (isPrototype) {
            var label = V("<text font-family='sans' fill='#000' font-size='8px'/>");
            label.text(portDesc.proposition);
            group.append(label);
            var labelBB = label.bbox(true);
          }

          var labelPad = 7;

          if (portType === 'assumption') {
            // put left
            var y = 20*index - 10*(total-1);
            pacman.translate( -width/2, y);
            pacman.rotate(135);
            pacman.attr({d: "M0,0 l 0 5 a5,5 0 1,1 5,-5 z"});
            if (isPrototype) {
              label.translate( -width/2 -labelBB.width - labelPad, y - labelBB.height/2 );
            }
          } else if (portType === 'conclusion') {
            // put right
            var y = 20*index - 10*(total-1);
            pacman.translate( width/2, y);
            pacman.rotate(135);
            pacman.attr({d: "M-5,-5 l 0 5 a5,5 0 1,0 5,-5 z"});
            if (isPrototype) {
              label.translate( width/2 + labelPad, y - labelBB.height/2 );
            }
          } else if (portType === 'local hypothesis') {
            // put below
            var x = 20*index - 10*(total-1);
            pacman.translate( x, height/2);
            pacman.rotate(225);
            pacman.attr({d: "M-5,-5 l 0 5 a5,5 0 1,0 5,-5 z"});
            if (isPrototype) {
              label.translate( x - labelBB.width/2, height/2 + labelPad);
            }
          } else {
            throw new Error("initialize(): Unknown portType");
          }
      });
    });
  },
  update: function () {
    // Do our on stuff
    var brokenPorts = this.model.get('brokenPorts') || {};

    _.each(this.vel.find(".port-body"), function (port) {
      if (V(port).attr('port') in brokenPorts) {
        V(port).attr('fill', '#F00');
      } else {
        V(port).attr('fill', '#777');
      }
    });

    if (this.model.get('conclusion')) {
      if (this.model.get('qed')) {
        V(this.vel.findOne(".body")).attr('fill','#0f0');
      } else {
        V(this.vel.findOne(".body")).attr('fill','#ecf0f1');
      };
    }

    // Just in case we use the attrs feature, let's call the jointjs update function
    joint.dia.ElementView.prototype.update.apply(this, arguments);
  }
});

joint.shapes.incredible.Link = joint.dia.Link.extend({

    arrowheadMarkup: [
        '<g class="marker-arrowhead-group marker-arrowhead-group-<%= end %>">',
        '<circle class="marker-arrowhead" end="<%= end %>" r="7"/>',
        // '<path class="marker-arrowhead" end="<%= end %>" d="M 26 0 L 0 13 L 26 26 z" />',
        '</g>'
    ].join(''),

    vertexMarkup: [
        '<g class="marker-vertex-group" transform="translate(<%= x %>, <%= y %>)">',
        '<circle class="marker-vertex" idx="<%= idx %>" r="10" />',
        '<g class="marker-vertex-remove-group">',
        '<path class="marker-vertex-remove-area" idx="<%= idx %>" d="M16,5.333c-7.732,0-14,4.701-14,10.5c0,1.982,0.741,3.833,2.016,5.414L2,25.667l5.613-1.441c2.339,1.317,5.237,2.107,8.387,2.107c7.732,0,14-4.701,14-10.5C30,10.034,23.732,5.333,16,5.333z" transform="translate(5, -33)"/>',
        '<path class="marker-vertex-remove" idx="<%= idx %>" transform="scale(.8) translate(9.5, -37)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z">',
        '<title>Remove vertex.</title>',
        '</path>',
        '</g>',
        '</g>'
    ].join(''),

    defaults: joint.util.deepSupplement({

        type: 'incredible.Link',

        attrs: {
            '.connection': { 'stroke-width': 2 },
            '.marker-vertex': { r: 7 }
        },

        // router: { name: 'orthogonal' },
        router: { name: 'manhattan', args: {
          paddingBox: function () {return {x: -5, y: -5, width: 10, height: 10}},
          startDirections: ['right'],
          endDirections: ['left']
        }},
        connector: { name: 'rounded', args: { radius: 10 }}

    }, joint.dia.Link.prototype.defaults)

});

<commit_msg>Wrap manhatten router to dynamically set start- and end orientation<commit_after>/*
 * Specification for Element representing blocks in the proof:
 *
 * Regular proof blocks:
 *   - The cell has an attribute 'rule', which is the rule object of the logic.
 *   - Every SVG element that is an magnet (magnet: true) has an attribute 'port',
 *     that corresponds to the name of the port in the logic, e.g. "in2" or "hyp".
 * Assumption blocks:
 *   - The cell has an attribute 'assumption', with the number of the assumption it
 *     represents.
 * Conclusion blocks:
 *   - The cell has an attribute 'conclusion', with the number of the assumption it
 *     represents.
 *
 * Elements with an attribute 'prototypeElement' set to true are not part of the
 * proof, but rather the elements one can drag into the paper.
 *
 * Furthermore, every magnet has an attribute 'direction', which is either "in" or
 * "out". This is used by the UI to make sure wires are connected properly.
 *
 */

shapes = {};

/*
 * How does jointJS find the view for a model?
 * It takes the 'type' field, of the model, excepts it to be foo.bar
 * and then looks at join.shapes[foo][bar+'View']....
 */

joint.shapes.incredible = {}

/*
 * The new way of doing things: No more visual information in the model,
 * the shape is calculated in the view.
 * The whole crazy `attrs` feature of JointJS is not used!
 */
joint.shapes.incredible.Generic = joint.shapes.basic.Generic.extend({
  defaults: joint.util.deepSupplement({
    type: 'incredible.Generic',
  }, joint.shapes.basic.Generic.prototype.defaults)
});

joint.shapes.incredible.GenericView = joint.dia.ElementView.extend({
  initialize: function () {
    joint.dia.ElementView.prototype.initialize.apply(this, arguments);

    // Listen to some incredible-specific change events
    this.listenTo(this.model, 'change:brokenPorts', this.update);
    if (this.model.get('conclusion')) {
      this.listenTo(this.model, 'change:qed', this.update);
    };
  },

  renderMarkup: function() {
    // These will not all be defined
    var rule = this.model.get('rule');
    var assumption = this.model.get('assumption');
    var conclusion = this.model.get('conclusion');
    var task = this.model.get('task');

    var isPrototype = this.model.get('prototypeElement');

    if (rule) {
      var ports = rule.ports;
      var portsList = _.sortBy(_.map(ports, function (v, i) {return _.extend({id: i}, v);}), 'id');
      var portsGroup = _.groupBy(portsList, "type");
    } else if (assumption) {
      // Fake port data for assumption and conclusion blocks
      var portsGroup= {'conclusion': [{
        id: 'out'
      }]};
    } else if (conclusion) {
      var portsGroup= {'assumption': [{
        id: 'in'
      }]};
    }

    this.vel.attr('magnet',false);

    var group = V("<g/>");
    this.vel.append(group);

    var text = V("<text class='label' font-family='sans' fill='black'/>");
    if (rule) {
      text.text(rule.id);
    } else if (assumption) {
      text.text(task.assumptions[assumption-1]);
    } else if (conclusion) {
      text.text(task.conclusions[conclusion-1]);
    }

    group.append(text);
    textBB = text.bbox(true);
    // center text
    text.translate(- textBB.width/2, - textBB.height/2);

    var rect = V("<rect class='body' fill='#ecf0f1' rx='5' ry='5' stroke='#bdc3c7' stroke-opacity='0.5'/>");

    // Calculate minimum width/height based on number of ports and label length
    var height = 35;
    var width = 80;
    _.each(portsGroup, function (thesePorts, portType) {
      var total = _.size(thesePorts);
      if (portType == 'local hypothesis') {
        width = Math.max(width, 20 * total -5);
      } else {
        height = Math.max(height, 20 * total -5);
      }
    });
    width = Math.max(width, textBB.width + 10);
    height = Math.max(height, textBB.height + 10);

    rect.attr({width: width, height: height});
    rect.translate(-width/2,-height/2);
    group.prepend(rect);

    _.each(portsGroup, function (thesePorts, portType) {
      var total = _.size(thesePorts);
      _.each(thesePorts, function (portDesc, index) {
          var direction = ({assumption: 'in', conclusion: 'out', 'local hypothesis': 'out'})[portType];
          var orientation = ({assumption: 'left', conclusion: 'right', 'local hypothesis': 'bottom'})[portType];
          var pacman = V('<path class="port-body" stroke="none" fill="#777" magnet="true"/>');
          pacman.attr({port: portDesc.id, direction: direction, orientation: orientation});

          group.append(pacman);

          if (isPrototype) {
            var label = V("<text font-family='sans' fill='#000' font-size='8px'/>");
            label.text(portDesc.proposition);
            group.append(label);
            var labelBB = label.bbox(true);
          }

          var labelPad = 7;

          if (portType === 'assumption') {
            // put left
            var y = 20*index - 10*(total-1);
            pacman.translate( -width/2, y);
            pacman.rotate(135);
            pacman.attr({d: "M0,0 l 0 5 a5,5 0 1,1 5,-5 z"});
            if (isPrototype) {
              label.translate( -width/2 -labelBB.width - labelPad, y - labelBB.height/2 );
            }
          } else if (portType === 'conclusion') {
            // put right
            var y = 20*index - 10*(total-1);
            pacman.translate( width/2, y);
            pacman.rotate(135);
            pacman.attr({d: "M-5,-5 l 0 5 a5,5 0 1,0 5,-5 z"});
            if (isPrototype) {
              label.translate( width/2 + labelPad, y - labelBB.height/2 );
            }
          } else if (portType === 'local hypothesis') {
            // put below
            var x = 20*index - 10*(total-1);
            pacman.translate( x, height/2);
            pacman.rotate(225);
            pacman.attr({d: "M-5,-5 l 0 5 a5,5 0 1,0 5,-5 z"});
            if (isPrototype) {
              label.translate( x - labelBB.width/2, height/2 + labelPad);
            }
          } else {
            throw new Error("initialize(): Unknown portType");
          }
      });
    });
  },
  update: function () {
    // Do our on stuff
    var brokenPorts = this.model.get('brokenPorts') || {};

    _.each(this.vel.find(".port-body"), function (port) {
      if (V(port).attr('port') in brokenPorts) {
        V(port).attr('fill', '#F00');
      } else {
        V(port).attr('fill', '#777');
      }
    });

    if (this.model.get('conclusion')) {
      if (this.model.get('qed')) {
        V(this.vel.findOne(".body")).attr('fill','#0f0');
      } else {
        V(this.vel.findOne(".body")).attr('fill','#ecf0f1');
      };
    }

    // Just in case we use the attrs feature, let's call the jointjs update function
    joint.dia.ElementView.prototype.update.apply(this, arguments);
  }
});

joint.shapes.incredible.Link = joint.dia.Link.extend({

    arrowheadMarkup: [
        '<g class="marker-arrowhead-group marker-arrowhead-group-<%= end %>">',
        '<circle class="marker-arrowhead" end="<%= end %>" r="7"/>',
        // '<path class="marker-arrowhead" end="<%= end %>" d="M 26 0 L 0 13 L 26 26 z" />',
        '</g>'
    ].join(''),

    vertexMarkup: [
        '<g class="marker-vertex-group" transform="translate(<%= x %>, <%= y %>)">',
        '<circle class="marker-vertex" idx="<%= idx %>" r="10" />',
        '<g class="marker-vertex-remove-group">',
        '<path class="marker-vertex-remove-area" idx="<%= idx %>" d="M16,5.333c-7.732,0-14,4.701-14,10.5c0,1.982,0.741,3.833,2.016,5.414L2,25.667l5.613-1.441c2.339,1.317,5.237,2.107,8.387,2.107c7.732,0,14-4.701,14-10.5C30,10.034,23.732,5.333,16,5.333z" transform="translate(5, -33)"/>',
        '<path class="marker-vertex-remove" idx="<%= idx %>" transform="scale(.8) translate(9.5, -37)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z">',
        '<title>Remove vertex.</title>',
        '</path>',
        '</g>',
        '</g>'
    ].join(''),

    defaults: joint.util.deepSupplement({

        type: 'incredible.Link',

        attrs: {
            '.connection': { 'stroke-width': 2 },
            '.marker-vertex': { r: 7 }
        },

        // router: { name: 'orthogonal' },

	/*
        router: { name: 'manhattan', args: {
          paddingBox: function () {return {x: -5, y: -5, width: 10, height: 10}},
          startDirections: ['right'],
          endDirections: ['left']
        }},
	*/
        router: { name: 'wrappedmanhattan' },
        connector: { name: 'rounded', args: { radius: 10 }}

    }, joint.dia.Link.prototype.defaults)

});

joint.routers.wrappedmanhattan = (function (){
  var manhattan = joint.routers.manhattan;

  return function(vertices, opt, linkView) {
    var startDirections = ['right']; // somewhat sensible default
    var endDirections = ['left']; // somewhat sensible default
    if (this.sourceMagnet) {
      startDirections = [this.sourceMagnet.getAttribute('orientation')];
    }
    if (this.targetMagnet) {
      endDirections = [this.targetMagnet.getAttribute('orientation')];
    }

    var args = {
      paddingBox: function () {return {x: -5, y: -5, width: 10, height: 10}},
      startDirections: startDirections,
      endDirections: endDirections
    }
    return manhattan.call(this, vertices, _.extend({},args,opt), linkView);
  };
})();

<|endoftext|>