<commit_before>import Ember from 'ember';
import THREE from "npm:three";
import config from 'explorviz-ui-frontend/config/environment';
import THREEPerformance from 'explorviz-ui-frontend/mixins/threejs-performance';

const {Component, inject, Evented} = Ember;

/**
* This component contains the core mechanics of the different (three.js-based)
* renderer. All functions below are called in a determined order, hence you only
* need to override them in your custom renderer.
*
* See {{#crossLink "Landscape-Rendering"}}{{/crossLink}} or
* {{#crossLink "Application-Rendering"}}{{/crossLink}} for example usage.
*
* Call order:
*
* 1.
*
* @class Rendering-Core-Component
* @extends Ember.Component
*
* @module explorviz
* @submodule visualization.rendering
*/
export default Component.extend(Evented, THREEPerformance, {

  state: null,

  // Declare url-builder service
  urlBuilder: inject.service("url-builder"),

  // Declare view-importer service
  viewImporter: inject.service("view-importer"),

  reloadHandler: inject.service("reload-handler"),
  landscapeRepo: inject.service("repos/landscape-repository"),
  renderingService: inject.service(),

  classNames: ['viz'],

  scene : null,
  webglrenderer: null,
  camera: null,

  canvas: null,

  font: null,
  animationFrameId: null,

  initDone: false,

  // @Override
  /**
   * This overridden Ember Component lifecycle hook enables calling
   * ExplorViz's setup code for actual rendering and custom listeners.
   *
   * @method didRender
   */
  didRender(){
    this._super(...arguments);
    this.initRendering();
    this.initListener();
  },

  // @Override
  /**
   * This overridden Ember Component lifecycle hook enables calling
   * ExplorViz's custom cleanup code.
   *
   * @method willDestroyElement
   */
  willDestroyElement() {
    this._super(...arguments);
    this.cleanup();
  },

  /**
   * This function is called once on the didRender event. Inherit this function
   * to call other important function, e.g. "initInteraction" as shown in
   * {{#crossLink "Landscape-Rendering/initInteraction:method"}}{{/crossLink}}.
   *
   * @method initRendering
   */
  initRendering() {

    this.debug('init rendering-core');

    const self = this;

    // get size if outer ember div
    const height = this.$()[0].clientHeight;
    const width = this.$()[0].clientWidth;

    const canvas = this.$('#threeCanvas')[0];

    this.set('canvas', canvas);

    this.set('scene', new THREE.Scene());
    this.set('scene.background', new THREE.Color(0xffffff));

    this.set('camera', new THREE.PerspectiveCamera(75, width / height, 0.1, 1000));

    this.set('webglrenderer', new THREE.WebGLRenderer({
      antialias: true,
      canvas: canvas
    }));
    this.get('webglrenderer').setPixelRatio(window.devicePixelRatio);
    this.get('webglrenderer').setSize(width, height);

    // Rendering loop //
    function render() {
      const animationId = requestAnimationFrame(render);
      self.set('animationFrameId', animationId);

      if(config.environment === "development" || config.environment === "akr") {
        self.get('threexStats').update(self.get('webglrenderer'));
        self.get('stats').begin();
      }

      self.get('webglrenderer').render(self.get('scene'), self.get('camera'));

      if(config.environment === "development" || config.environment === "akr") {
        self.get('stats').end();
      }

    }

    render();

    ////////////////////

    // load font for labels and synchronously proceed with populating the scene
    new THREE.FontLoader()
      .load('three.js/fonts/roboto_mono_bold_typeface.json', function(font) {

      self.set('font', font);
      self.set('initDone', true);
      self.populateScene();
      // import new view
      self.importView();

    });

  },

  initListener() {

    const self = this;

    this.$(window).on('resize.visualization', function(){
      const outerDiv = this.$('.viz')[0];

      if(outerDiv) {

        const height = Math.round(this.$('.viz').height());
        const width = Math.round(this.$('.viz').width());

        self.set('camera.aspect', width / height);
        self.get('camera').updateProjectionMatrix();

        self.get('webglrenderer').setSize(width, height);

        self.onResized();
      }
    });

    this.get('viewImporter').on('transmitView', function(newState) {
        self.set('newState', newState);
    });

    this.get('renderingService').on('reSetupScene', function() {
      self.onReSetupScene();
    });

    this.get('urlBuilder').on('requestURL', function() {
      const state = {};

      // get timestamp
      state.timestamp = self.get('landscapeRepo.latestLandscape')
        .get('timestamp');

      // get latestApp, may be null
      const latestMaybeApp = self.get('landscapeRepo.latestApplication');
      state.appID = latestMaybeApp ? latestMaybeApp.get('id') : null;

      state.camX = self.get('camera').position.x;
      state.camY = self.get('camera').position.y;
      state.camZ = self.get('camera').position.z;

      if(state.appID){
        state.appCondition = self.computeAppCondition();
      }
      else{
        state.landscapeCondition = self.computeLandscapeCondition();
      }

      // Passes the state from component via service to controller
      self.get('urlBuilder').transmitState(state);
    });

    this.get('landscapeRepo').on("updated", function() {
      self.onUpdated();
    });

  },

  /*
   * This method is used to collect data about the landscape.
   * Ids of closed systems and nodegroups are stored in an array
   */
  computeLandscapeCondition(){

    const systems = this.get('landscapeRepo.latestLandscape').get('systems');
    const condition = [];

    systems.forEach(function(system) {

      let isRequestObject = false;

      if (!isRequestObject && system.get('name') === "Requests") {
        isRequestObject = true;
      }
      if(!isRequestObject){

        // Handle closed systems and add id to array
        if(!system.get('opened')){
          condition.push(system.get('id'));
        }

        // Handle opened systems
        else{
          // Handle closed nodegroups and add id to array
          const nodegroups = system.get('nodegroups');
          nodegroups.forEach(function(nodegroup){
            if(!nodegroup.get('opened')){
              condition.push(nodegroup.get('id'));
            }
          });
        }
      }
    });
    return condition;
  },

  /*
   * This method is used to collect data about the application.
   * Names of opened packages are stored in an array.
   */
  computeAppCondition(){
    const self = this;
    const components = this.get('landscapeRepo.latestApplication').get('components');
    const condition = [];
    console.log("compute appcondition");
    components.forEach(function(component) {
      // Handle opened packages and add name to array
      if(component.get('opened')){
        condition.push(component.get('name'));
        self.computeAppChildrenCondition(component,condition);
      }
      // Handle closed and highlighted packages
      else if(component.get('highlighted')){
        condition.push(component.get('name').concat("highlighted"));
      }
    });
    return condition;
  },

  /*
   *  This method is used to calculate the state of the subpackages
   */
  computeAppChildrenCondition(parent,condition){
    const self = this;
    let enties = parent.get('children');

    enties.forEach(function(entity) {
      if(entity.get('opened')){
        condition.push(entity.get('name'));
        self.computeAppChildrenCondition(entity,condition);
      }
      else if(entity.get('highlighted')){
        condition.push(entity.get('name').concat("highlighted"));
      }
    });
  },

  /*
   *  This method is used to open and close specified systems
   *  and nodegroups. The array 'condition' contains the ids of
   *  of all closed systems and nodegroups
   */
  applyLandscapeCondition(landscape){

    const self = this;
    const systems = landscape.get('systems');

    systems.forEach(function(system) {
      let isRequestObject = false;

      if (!isRequestObject && system.get('name') === "Requests") {
        isRequestObject = true;
      }

      if(!isRequestObject){

        // Open system
        system.setOpened(true);
        for (var i = 0; i < self.get('condition').length; i++) {

          // Close system if id is in condition
          if(parseFloat(system.get('id')) === parseFloat(self.get('condition')[i])){
            system.setOpened(false);
          }
        }

        // Check condition of nodegroups if system is opened
        if(system.get('opened')){
          const nodegroups = system.get('nodegroups');

          nodegroups.forEach(function(nodegroup) {

            // Open nodegroup
            nodegroup.setOpened(true);
            for (var i = 0; i < self.get('condition').length; i++) {

              // Close nodegroup if id is in condition
              if(parseFloat(nodegroup.get('id')) === parseFloat(self.get('condition')[i])){
                nodegroup.setOpened(false);
              }
            }
          });
        }
      }
    });
  },

  applyAppCondition(application){

    const self = this;
    if(application){

      const components = application.get('components');
      if(components){
        components.forEach(function(component) {
          // Close component
          component.setOpenedStatus(false);
          for (var i = 0; i < self.get('condition').length; i++) {
            // Open component if name is in condition
            if(component.get('name') === self.get('condition')[i]){
              component.setOpenedStatus(true);
              self.applyAppConditionChildren(component);
            }
            // case not opened => maybe highlighted
            else if(component.get('name').concat("highlighted") === self.get('condition')[i]) {
              component.set('highlighted',true);
            }
          }
        });
      self.cleanAndUpdateScene();
      }
    }
  },

  applyAppConditionChildren(entity){
    const self = this;
    if(entity){
      const components = entity.get('children');
      if(components){
        components.forEach(function(component) {

          component.setOpenedStatus(false);
          for (var i = 0; i < self.get('condition').length; i++) {
            // Close system if id is in condition
            if(component.get('name') === self.get('condition')[i]){
              component.setOpenedStatus(true);
              self.applyAppConditionChildren(component);
            }
            // case not opened => maybe highlighted
            else if(component.get('name').concat("highlighted") === self.get('condition')[i]) {
              component.set('highlighted',true);
            }
          }
        });
      }
    }
  },

  /**
    This method is used to update the camera with query parameters
  */
  importView(){

    const self = this;

    this.get('viewImporter').requestView();

    const camX = this.get('newState').camX;
    const camY = this.get('newState').camY;
    const camZ = this.get('newState').camZ;

    this.set('condition', this.get('newState').condition);

    if(!isNaN(camX)){
      this.get('camera').position.x = camX;
    }
    if(!isNaN(camY)){
      this.get('camera').position.y = camY;
    }
    if(!isNaN(camZ)){
      this.get('camera').position.z = camZ;
    }
    this.get('camera').updateProjectionMatrix();

    // load actual landscape
    const timestamp = this.get('newState').timestamp;
    const appID = this.get('newState').appID;

    // Stop timeline
    self.get('reloadHandler').stopExchange();

    if(timestamp) {
      loadLandscape();
    }

    // Callback to wait until landscape is loaded
    function caller(callbackFunction){
      self.get('reloadHandler').loadLandscapeById(timestamp, appID);
      // Do callback stuff after landscape is loaded
      callbackFunction();
    }

    // Pass callback function
    function loadLandscape(){
      caller(function() {
        if(appID){
          self.applyAppCondition(self.get('landscapeRepo.latestApplication'));
        }
        else{
          self.applyLandscapeCondition(self.get('landscapeRepo.latestLandscape'));
        }
      });
    }
  },

  /**
   * This function is called once on initRendering and everytime at the end of
   * "cleanAndUpdateScene". Inherit this function to insert objects in the
   * Three.js scene. Have a look at
   * {{#crossLink "Landscape-Rendering/cleanAndUpdateScene:method"}}
   * {{/crossLink}}
   * or
   * {{#crossLink "Application-Rendering/cleanAndUpdateScene:method"}}
   * {{/crossLink}}
   * for examplary usage.
   *
   * @method populateScene
   */
  populateScene() {},

  /**
   * This function is called when the willDestroyElement event is fired. Inherit this
   * function to cleanup custom properties or unbind listener
   * as shown in {{#crossLink "Landscape-Rendering"}}{{/crossLink}}.
   *
   * @method cleanup
   */
  cleanup() {
    cancelAnimationFrame(this.get('animationFrameId'));

    this.set('scene', null);
    this.set('webglrenderer', null);

    // clean up WebGL rendering context by forcing context loss
    var gl = this.get('canvas').getContext('webgl');
    gl.getExtension('WEBGL_lose_context').loseContext();

    this.set('camera', null);
    this.get('urlBuilder').off('requestURL');

    this.removePerformanceMeasurement();

    this.$(window).off('resize.visualization');
    this.get('viewImporter').off('transmitView');
    this.get('renderingService').off('reSetupScene');
    this.get('landscapeRepo').off('updated');
  },

  /**
   * Inherit this function to update the scene with a new renderingModel. It
   * automatically removes every mesh from the scene and finally calls
   * the (overridden) "populateScene" function. Add your custom code
   * as shown in landscape-rendering.
   *
   * @method cleanAndUpdateScene
   */
  cleanAndUpdateScene() {
    const scene = this.get('scene');

    removeAllChildren(scene);

    function removeAllChildren(entity) {
      for (let i = entity.children.length - 1; i >= 0 ; i--) {
        let child = entity.children[i];

        removeAllChildren(child);

        if (child.type !== 'AmbientLight' && child.type !== 'SpotLight' && child.type !== 'DirectionalLight') {
          if(child.type !== 'Object3D') {
            child.geometry.dispose();
            child.material.dispose();
          }
          entity.remove(child);
        }
      }
    }
    this.populateScene();
  },

  /**
   * This function is called automatically when a new landscape was fetched. It
   * is executed before
   * {{#crossLink "Rendering-Core/cleanAndUpdateScene:method"}}{{/crossLink}}.
   * Inherit this function to preprocess the
   * {{#crossLink "Landscape"}}{{/crossLink}} for rendering, e.g. filter some
   * value.
   *
   * See {{#crossLink "Application-Rendering"}}{{/crossLink}} for example usage.
   *
   * @method preProcessEntity
   */
  preProcessEntity() {},

  // Listener-Callbacks. Override in extending components

  onReSetupScene() {
    this.cleanAndUpdateScene();
  },

  onUpdated() {
    this.cleanAndUpdateScene();
  },

  onResized() {
    this.cleanAndUpdateScene();
  }

});
<commit_msg>added: import application view with highlighted clazzes<commit_after>import Ember from 'ember';
import THREE from "npm:three";
import config from 'explorviz-ui-frontend/config/environment';
import THREEPerformance from 'explorviz-ui-frontend/mixins/threejs-performance';

const {Component, inject, Evented} = Ember;

/**
* This component contains the core mechanics of the different (three.js-based)
* renderer. All functions below are called in a determined order, hence you only
* need to override them in your custom renderer.
*
* See {{#crossLink "Landscape-Rendering"}}{{/crossLink}} or
* {{#crossLink "Application-Rendering"}}{{/crossLink}} for example usage.
*
* Call order:
*
* 1.
*
* @class Rendering-Core-Component
* @extends Ember.Component
*
* @module explorviz
* @submodule visualization.rendering
*/
export default Component.extend(Evented, THREEPerformance, {

  state: null,

  // Declare url-builder service
  urlBuilder: inject.service("url-builder"),

  // Declare view-importer service
  viewImporter: inject.service("view-importer"),

  reloadHandler: inject.service("reload-handler"),
  landscapeRepo: inject.service("repos/landscape-repository"),
  renderingService: inject.service(),

  classNames: ['viz'],

  scene : null,
  webglrenderer: null,
  camera: null,

  canvas: null,

  font: null,
  animationFrameId: null,

  initDone: false,

  appCondition: [],

  // @Override
  /**
   * This overridden Ember Component lifecycle hook enables calling
   * ExplorViz's setup code for actual rendering and custom listeners.
   *
   * @method didRender
   */
  didRender(){
    this._super(...arguments);
    this.initRendering();
    this.initListener();
  },

  // @Override
  /**
   * This overridden Ember Component lifecycle hook enables calling
   * ExplorViz's custom cleanup code.
   *
   * @method willDestroyElement
   */
  willDestroyElement() {
    this._super(...arguments);
    this.cleanup();
  },

  /**
   * This function is called once on the didRender event. Inherit this function
   * to call other important function, e.g. "initInteraction" as shown in
   * {{#crossLink "Landscape-Rendering/initInteraction:method"}}{{/crossLink}}.
   *
   * @method initRendering
   */
  initRendering() {

    this.debug('init rendering-core');

    const self = this;

    // get size if outer ember div
    const height = this.$()[0].clientHeight;
    const width = this.$()[0].clientWidth;

    const canvas = this.$('#threeCanvas')[0];

    this.set('canvas', canvas);

    this.set('scene', new THREE.Scene());
    this.set('scene.background', new THREE.Color(0xffffff));

    this.set('camera', new THREE.PerspectiveCamera(75, width / height, 0.1, 1000));

    this.set('webglrenderer', new THREE.WebGLRenderer({
      antialias: true,
      canvas: canvas
    }));
    this.get('webglrenderer').setPixelRatio(window.devicePixelRatio);
    this.get('webglrenderer').setSize(width, height);

    // Rendering loop //
    function render() {
      const animationId = requestAnimationFrame(render);
      self.set('animationFrameId', animationId);

      if(config.environment === "development" || config.environment === "akr") {
        self.get('threexStats').update(self.get('webglrenderer'));
        self.get('stats').begin();
      }

      self.get('webglrenderer').render(self.get('scene'), self.get('camera'));

      if(config.environment === "development" || config.environment === "akr") {
        self.get('stats').end();
      }

    }

    render();

    ////////////////////

    // load font for labels and synchronously proceed with populating the scene
    new THREE.FontLoader()
      .load('three.js/fonts/roboto_mono_bold_typeface.json', function(font) {

      self.set('font', font);
      self.set('initDone', true);
      self.populateScene();
      // import new view
      self.importView();

    });

  },

  initListener() {

    const self = this;

    this.$(window).on('resize.visualization', function(){
      const outerDiv = this.$('.viz')[0];

      if(outerDiv) {

        const height = Math.round(this.$('.viz').height());
        const width = Math.round(this.$('.viz').width());

        self.set('camera.aspect', width / height);
        self.get('camera').updateProjectionMatrix();

        self.get('webglrenderer').setSize(width, height);

        self.onResized();
      }
    });

    this.get('viewImporter').on('transmitView', function(newState) {
        self.set('newState', newState);
    });

    this.get('renderingService').on('reSetupScene', function() {
      self.onReSetupScene();
    });

    this.get('urlBuilder').on('requestURL', function() {
      const state = {};

      // get timestamp
      state.timestamp = self.get('landscapeRepo.latestLandscape')
        .get('timestamp');

      // get latestApp, may be null
      const latestMaybeApp = self.get('landscapeRepo.latestApplication');
      state.appID = latestMaybeApp ? latestMaybeApp.get('id') : null;

      state.camX = self.get('camera').position.x;
      state.camY = self.get('camera').position.y;
      state.camZ = self.get('camera').position.z;

      if(state.appID){
        self.set('appCondition',[]);
        self.computeAppCondition(
          self.get('landscapeRepo.latestApplication').get('components'),
          self.get('landscapeRepo.latestApplication').get('clazzes'));
        state.appCondition = self.get('appCondition');
      }
      else{
        state.landscapeCondition = self.computeLandscapeCondition();
      }

      // Passes the state from component via service to controller
      self.get('urlBuilder').transmitState(state);
    });

    this.get('landscapeRepo').on("updated", function() {
      self.onUpdated();
    });

  },

  /*
   *  This method is used to collect data about the landscape.
   *  Ids of closed systems and nodegroups are stored in an array
   */
  computeLandscapeCondition(){

    const systems = this.get('landscapeRepo.latestLandscape').get('systems');
    const condition = [];

    systems.forEach(function(system) {

      let isRequestObject = false;

      // Find requests
      if (!isRequestObject && system.get('name') === "Requests") {
        isRequestObject = true;
      }
      // Exclude requests
      if(!isRequestObject){
        // Handle closed systems and add id to array
        if(!system.get('opened')){
          condition.push(system.get('id'));
        }
        // Handle opened systems
        else{
          // Handle closed nodegroups and add id to array
          const nodegroups = system.get('nodegroups');
          nodegroups.forEach(function(nodegroup){
            if(!nodegroup.get('opened')){
              condition.push(nodegroup.get('id'));
            }
          });
        }
      }
    });
    return condition;
  },

  /*
   *  This method is used to collect data about the application.
   *  Names of opened packages are stored in an array.
   *  Furthermore the name+highlighted of the
   *  highlighted package is stored
   */
  computeAppCondition(components, clazzes){
    const self = this;

    if(clazzes){
      clazzes.forEach(function(clazz) {
        if(clazz.get('highlighted')){
          self.get('appCondition').push(clazz.get('name').concat("highlighted"));
        }
      });
    }

    components.forEach(function(component) {
      // Handle opened packages and add name to array
      if(component.get('opened')){
        self.get('appCondition').push(component.get('name'));
        self.computeAppCondition(component.get('children'), component.get('clazzes'));
      }
      // Handle closed and highlighted packages
      else if(component.get('highlighted')){
        self.get('appCondition').push(component.get('name').concat("highlighted"));
      }
    });
  },

  /*
   *  This method is used to open and close specified systems
   *  and nodegroups. The array 'condition' contains the ids of
   *  of all closed systems and nodegroups
   */
  applyLandscapeCondition(landscape){

    const self = this;
    const systems = landscape.get('systems');

    systems.forEach(function(system) {
      let isRequestObject = false;

      if (!isRequestObject && system.get('name') === "Requests") {
        isRequestObject = true;
      }

      if(!isRequestObject){

        // Open system
        system.setOpened(true);
        for (var i = 0; i < self.get('condition').length; i++) {

          // Close system if id is in condition
          if(parseFloat(system.get('id')) === parseFloat(self.get('condition')[i])){
            system.setOpened(false);
          }
        }

        // Check condition of nodegroups if system is opened
        if(system.get('opened')){
          const nodegroups = system.get('nodegroups');

          nodegroups.forEach(function(nodegroup) {

            // Open nodegroup
            nodegroup.setOpened(true);
            for (var i = 0; i < self.get('condition').length; i++) {

              // Close nodegroup if id is in condition
              if(parseFloat(nodegroup.get('id')) === parseFloat(self.get('condition')[i])){
                nodegroup.setOpened(false);
              }
            }
          });
        }
      }
    });
  },

  applyAppCondition(application){

    const self = this;
    if(application){

      const components = application.get('components');
      const clazzes = application.get('clazzes');

      if(clazzes){
        clazzes.forEach(function(clazz) {
          for (var i = 0; i < self.get('condition').length; i++) {
            // case not opened => maybe highlighted
            if(clazz.get('name').concat("highlighted") === self.get('condition')[i]) {
              clazz.set('highlighted',true);
            }
          }
        });
      }

      if(components){
        components.forEach(function(component) {
          // Close component
          component.setOpenedStatus(false);
          for (var i = 0; i < self.get('condition').length; i++) {
            // Open component if name is in condition
            if(component.get('name') === self.get('condition')[i]){
              component.setOpenedStatus(true);
              self.applyAppConditionChildren(component);
            }
            // case not opened => maybe highlighted
            else if(component.get('name').concat("highlighted") === self.get('condition')[i]) {
              component.set('highlighted',true);
            }
          }
        });
      self.cleanAndUpdateScene();
      }
    }
  },

  applyAppConditionChildren(entity){
    const self = this;
    if(entity){
      const components = entity.get('children');

      const clazzes = entity.get('clazzes');

      if(clazzes){
        clazzes.forEach(function(clazz) {
          for (var i = 0; i < self.get('condition').length; i++) {
            // case not opened => maybe highlighted
            if(clazz.get('name').concat("highlighted") === self.get('condition')[i]) {
              clazz.set('highlighted',true);
            }
          }
        });
      }

      if(components){
        components.forEach(function(component) {

          component.setOpenedStatus(false);
          for (var i = 0; i < self.get('condition').length; i++) {
            // Close system if id is in condition
            if(component.get('name') === self.get('condition')[i]){
              component.setOpenedStatus(true);
              self.applyAppConditionChildren(component);
            }
            // case not opened => maybe highlighted
            else if(component.get('name').concat("highlighted") === self.get('condition')[i]) {
              component.set('highlighted',true);
            }
          }
        });
      }
    }
  },

  /**
    This method is used to update the camera with query parameters
  */
  importView(){

    const self = this;

    this.get('viewImporter').requestView();

    const camX = this.get('newState').camX;
    const camY = this.get('newState').camY;
    const camZ = this.get('newState').camZ;

    this.set('condition', this.get('newState').condition);

    if(!isNaN(camX)){
      this.get('camera').position.x = camX;
    }
    if(!isNaN(camY)){
      this.get('camera').position.y = camY;
    }
    if(!isNaN(camZ)){
      this.get('camera').position.z = camZ;
    }
    this.get('camera').updateProjectionMatrix();

    // load actual landscape
    const timestamp = this.get('newState').timestamp;
    const appID = this.get('newState').appID;

    // Stop timeline
    self.get('reloadHandler').stopExchange();

    if(timestamp) {
      loadLandscape();
    }

    // Callback to wait until landscape is loaded
    function caller(callbackFunction){
      self.get('reloadHandler').loadLandscapeById(timestamp, appID);
      // Do callback stuff after landscape is loaded
      callbackFunction();
    }

    // Pass callback function
    function loadLandscape(){
      caller(function() {
        if(appID){
          self.applyAppCondition(self.get('landscapeRepo.latestApplication'));
        }
        else{
          self.applyLandscapeCondition(self.get('landscapeRepo.latestLandscape'));
        }
      });
    }
  },

  /**
   * This function is called once on initRendering and everytime at the end of
   * "cleanAndUpdateScene". Inherit this function to insert objects in the
   * Three.js scene. Have a look at
   * {{#crossLink "Landscape-Rendering/cleanAndUpdateScene:method"}}
   * {{/crossLink}}
   * or
   * {{#crossLink "Application-Rendering/cleanAndUpdateScene:method"}}
   * {{/crossLink}}
   * for examplary usage.
   *
   * @method populateScene
   */
  populateScene() {},

  /**
   * This function is called when the willDestroyElement event is fired. Inherit this
   * function to cleanup custom properties or unbind listener
   * as shown in {{#crossLink "Landscape-Rendering"}}{{/crossLink}}.
   *
   * @method cleanup
   */
  cleanup() {
    cancelAnimationFrame(this.get('animationFrameId'));

    this.set('scene', null);
    this.set('webglrenderer', null);

    // clean up WebGL rendering context by forcing context loss
    var gl = this.get('canvas').getContext('webgl');
    gl.getExtension('WEBGL_lose_context').loseContext();

    this.set('camera', null);
    this.get('urlBuilder').off('requestURL');

    this.removePerformanceMeasurement();

    this.$(window).off('resize.visualization');
    this.get('viewImporter').off('transmitView');
    this.get('renderingService').off('reSetupScene');
    this.get('landscapeRepo').off('updated');
  },

  /**
   * Inherit this function to update the scene with a new renderingModel. It
   * automatically removes every mesh from the scene and finally calls
   * the (overridden) "populateScene" function. Add your custom code
   * as shown in landscape-rendering.
   *
   * @method cleanAndUpdateScene
   */
  cleanAndUpdateScene() {
    const scene = this.get('scene');

    removeAllChildren(scene);

    function removeAllChildren(entity) {
      for (let i = entity.children.length - 1; i >= 0 ; i--) {
        let child = entity.children[i];

        removeAllChildren(child);

        if (child.type !== 'AmbientLight' && child.type !== 'SpotLight' && child.type !== 'DirectionalLight') {
          if(child.type !== 'Object3D') {
            child.geometry.dispose();
            child.material.dispose();
          }
          entity.remove(child);
        }
      }
    }
    this.populateScene();
  },

  /**
   * This function is called automatically when a new landscape was fetched. It
   * is executed before
   * {{#crossLink "Rendering-Core/cleanAndUpdateScene:method"}}{{/crossLink}}.
   * Inherit this function to preprocess the
   * {{#crossLink "Landscape"}}{{/crossLink}} for rendering, e.g. filter some
   * value.
   *
   * See {{#crossLink "Application-Rendering"}}{{/crossLink}} for example usage.
   *
   * @method preProcessEntity
   */
  preProcessEntity() {},

  // Listener-Callbacks. Override in extending components

  onReSetupScene() {
    this.cleanAndUpdateScene();
  },

  onUpdated() {
    this.cleanAndUpdateScene();
  },

  onResized() {
    this.cleanAndUpdateScene();
  }

});
<|endoftext|>


<commit_before>"use strict";

treeherder.controller('BugFilerCtrl', [
    '$scope', '$rootScope', '$uibModalInstance', '$http', 'summary', 'thBugzillaProductObject',
    'thPinboard', 'thEvents', 'fullLog', 'parsedLog', 'reftest', 'selectedJob', 'allFailures',
    'thNotify',
    function BugFilerCtrl(
        $scope, $rootScope, $uibModalInstance, $http, summary, thBugzillaProductObject,
        thPinboard, thEvents, fullLog, parsedLog, reftest, selectedJob, allFailures,
        thNotify) {

        $scope.omittedLeads = ["TEST-UNEXPECTED-FAIL", "PROCESS-CRASH", "TEST-UNEXPECTED-ERROR"];

        /**
         *  'enter' from the product search input should initiate the search
         */
        $scope.productSearchEnter = function(ev) {
            if (ev.keyCode === 13) {
                $scope.findProduct();
            }
        };

        /*
         **
         */
        $scope.isReftest = function() {
            return reftest !== "";
        };

        $scope.parsedLog = parsedLog;
        $scope.fullLog = fullLog;
        if ($scope.isReftest()) {
            $scope.reftest = reftest;
        }

        /**
         *  Pre-fill the form with information/metadata from the failure
         */
        $scope.initiate = function() {
            thPinboard.pinJob($rootScope.selectedJob);
            var thisFailure = "";
            for (var i = 0; i < allFailures.length; i++) {
                for (var j=0; j < $scope.omittedLeads.length; j++) {
                    if (allFailures[i][0].search($scope.omittedLeads[j]) >= 0 && allFailures[i].length > 1) {
                        allFailures[i].shift();
                    }
                }
                if (i !== 0) {
                    thisFailure += "\n";
                }
                thisFailure += allFailures[i].join(" | ");
            }
            $scope.thisFailure = thisFailure;

            $scope.findProduct();
        };

        $uibModalInstance.parsedSummary = "";
        $uibModalInstance.initiate = $scope.initiate;
        $uibModalInstance.possibleFilename = "";

        /*
         *  Remove extraneous junk from the start of the summary line
         *  and try to find the failing test name from what's left
         */
        $uibModalInstance.parseSummary = function(summary) {
            summary = summary.split(" | ");

            for (var i=0; i < $scope.omittedLeads.length; i++) {
                if (summary[0].search($scope.omittedLeads[i]) >= 0 && summary.length > 1) {
                    summary.shift();
                }
            }

            $uibModalInstance.possibleFilename = summary[0].split("/").pop();

            return [summary, $uibModalInstance.possibleFilename];
        };

        $uibModalInstance.parsedSummary = $uibModalInstance.parseSummary(summary);
        $scope.modalSummary = "Intermittent " + $uibModalInstance.parsedSummary[0].join(" | ");

        $scope.toggleFilerSummaryVisibility = function() {
            $scope.isFilerSummaryVisible = !$scope.isFilerSummaryVisible;
        };

        $scope.isFilerSummaryVisible = false;

        /*
         *  Attempt to find a good product/component for this failure
         */
        $scope.findProduct = function() {
            $scope.suggestedProducts = [];
            var failurePath = $uibModalInstance.parsedSummary[0][0];

            // If the "TEST-UNEXPECTED-foo" isn't one of the omitted ones, use the next piece in the summary
            if (failurePath.includes("TEST-UNEXPECTED-")) {
                failurePath = $uibModalInstance.parsedSummary[0][1];
            }
            var failurePathRoot = failurePath.split("/")[0];

            // Look up the product via the root of the failure's file path
            if (thBugzillaProductObject[failurePathRoot]) {
                $scope.suggestedProducts.push(thBugzillaProductObject[failurePathRoot][0]);
            }

            // Some job types are special, lets explicitly handle them.
            if (selectedJob.job_group_name.includes("Web Platform")) {
                $scope.suggestedProducts.push("Testing :: web-platform-tests");
            }

            // Look up product suggestions via Bugzilla's api
            var productSearch = $scope.productSearch;

            if (productSearch) {
                $http.get("https://bugzilla.mozilla.org/rest/prod_comp_search/" + productSearch + "?limit=5").then(function(request) {
                    var data = request.data;
                    // We can't file unless product and component are provided, this api can return just product. Cut those out.
                    for (var i = data.products.length - 1; i >= 0; i--) {
                        if (!data.products[i].component) {
                            data.products.splice(i, 1);
                        }
                    }
                    $scope.suggestedProducts = [];
                    $scope.suggestedProducts = _.map(data.products, function(prod) {
                        if (prod.product && prod.component) {
                            return prod.product + " :: " + prod.component;
                        }
                        return prod.product;
                    });
                    $scope.selection.selectedProduct = $scope.suggestedProducts[0];
                });
            }

            $scope.selection.selectedProduct = $scope.suggestedProducts[0];
        };

        /*
         *  Same as clicking outside of the modal, but with a nice button-clicking feel...
         */
        $scope.cancelFiler = function() {
            $uibModalInstance.dismiss('cancel');
        };

        $scope.checkedLogLinks = {
            parsedLog: $scope.parsedLog,
            fullLog: $scope.fullLog,
            reftest: $scope.reftest
        };

        $scope.isIntermittent = true;

        /*
         *  Actually send the gathered information to bugzilla.
         */
        $scope.submitFiler = function() {
            var summarystring = $scope.modalSummary;
            var productString = "";
            var componentString = "";

            $scope.toggleForm(true);

            if ($scope.modalSummary.length > 255) {
                thNotify.send("Please ensure the summary is no more than 255 characters", "danger");
                $scope.toggleForm(false);
                return;
            }

            if ($scope.selection.selectedProduct) {
                var prodParts = $scope.selection.selectedProduct.split(" :: ");
                productString += prodParts[0];
                componentString += prodParts[1];
            } else {
                thNotify.send("Please select (or search and select) a product/component pair to continue", "danger");
                $scope.toggleForm(false);
                return;
            }

            var descriptionStrings = _.reduce($scope.checkedLogLinks, function(result, link) {
                if (link) {
                    result = result + link + "\n\n";
                }
                return result;
            }, "");
            if ($scope.modalComment) {
                descriptionStrings += $scope.modalComment;
            }

            var keywords = $scope.isIntermittent ? "intermittent-failure" : "";

            var blocks = $scope.modalBlocks;
            var dependsOn = $scope.modalDependsOn;
            var seeAlso = $scope.modalSeeAlso;

            // Fetch product information from bugzilla to get version numbers, then submit the new bug
            // Only request the versions because some products take quite a long time to fetch the full object
            $.ajax("https://bugzilla.mozilla.org/rest/product/" + productString + "?include_fields=versions").done(function(productJSON) {
                var productObject = productJSON.products[0];

                // Find the newest version for the product that is_active
                var version = _.findLast(productObject.versions, function(version) {
                    return version.is_active === true;
                });

                $http({
                    url: "api/bugzilla/create_bug/",
                    method: "POST",
                    data: {
                        "product": productString,
                        "component": componentString,
                        "summary": summarystring,
                        "keywords": keywords,
                        "version": version.name,
                        "blocks": blocks,
                        "depends_on": dependsOn,
                        "see_also": seeAlso,
                        "comment": descriptionStrings,
                        "comment_tags": "treeherder"
                    }
                }).then(function successCallback(json) {
                    if (json.data.failure) {
                        var errorString = "";
                        for (var i = 0; i < json.data.failure.length; i++) {
                            errorString += json.data.failure[i];
                        }
                        errorString = JSON.parse(errorString);
                        thNotify.send("Bugzilla error: " + errorString.message, "danger", true);
                        $scope.toggleForm(false);
                    } else {
                        // Auto-classify this failure now that the bug has been filed and we have a bug number
                        thPinboard.addBug({id:json.data.success});
                        $rootScope.$evalAsync($rootScope.$emit(thEvents.saveClassification));

                        // Open the newly filed bug in a new tab or window for further editing
                        window.open("https://bugzilla.mozilla.org/show_bug.cgi?id=" + json.data.success);
                        $scope.cancelFiler();
                    }
                }, function errorCallback(response) {
                    var failureString = "Bug Filer API returned status " + response.status + " (" + response.statusText + ")";
                    if (response.data && response.data.failure) {
                        failureString += "\n\n" + response.data.failure;
                    }
                    thNotify.send(failureString, "danger");
                    $scope.toggleForm(false);
                });
            });
        };

        /*
         *  Disable or enable form elements as needed at various points in the submission process
         */
        $scope.toggleForm = function(disabled) {
            $(':input','#modalForm').attr("disabled", disabled);
        };
    }
]);

<commit_msg>Bug 1324768 - Better notify if authentication fails when using the bug filer (#2035) r=wlach<commit_after>"use strict";

treeherder.controller('BugFilerCtrl', [
    '$scope', '$rootScope', '$uibModalInstance', '$http', 'summary', 'thBugzillaProductObject',
    'thPinboard', 'thEvents', 'fullLog', 'parsedLog', 'reftest', 'selectedJob', 'allFailures',
    'thNotify',
    function BugFilerCtrl(
        $scope, $rootScope, $uibModalInstance, $http, summary, thBugzillaProductObject,
        thPinboard, thEvents, fullLog, parsedLog, reftest, selectedJob, allFailures,
        thNotify) {

        $scope.omittedLeads = ["TEST-UNEXPECTED-FAIL", "PROCESS-CRASH", "TEST-UNEXPECTED-ERROR"];

        /**
         *  'enter' from the product search input should initiate the search
         */
        $scope.productSearchEnter = function(ev) {
            if (ev.keyCode === 13) {
                $scope.findProduct();
            }
        };

        /*
         **
         */
        $scope.isReftest = function() {
            return reftest !== "";
        };

        $scope.parsedLog = parsedLog;
        $scope.fullLog = fullLog;
        if ($scope.isReftest()) {
            $scope.reftest = reftest;
        }

        /**
         *  Pre-fill the form with information/metadata from the failure
         */
        $scope.initiate = function() {
            thPinboard.pinJob($rootScope.selectedJob);
            var thisFailure = "";
            for (var i = 0; i < allFailures.length; i++) {
                for (var j=0; j < $scope.omittedLeads.length; j++) {
                    if (allFailures[i][0].search($scope.omittedLeads[j]) >= 0 && allFailures[i].length > 1) {
                        allFailures[i].shift();
                    }
                }
                if (i !== 0) {
                    thisFailure += "\n";
                }
                thisFailure += allFailures[i].join(" | ");
            }
            $scope.thisFailure = thisFailure;

            $scope.findProduct();
        };

        $uibModalInstance.parsedSummary = "";
        $uibModalInstance.initiate = $scope.initiate;
        $uibModalInstance.possibleFilename = "";

        /*
         *  Remove extraneous junk from the start of the summary line
         *  and try to find the failing test name from what's left
         */
        $uibModalInstance.parseSummary = function(summary) {
            summary = summary.split(" | ");

            for (var i=0; i < $scope.omittedLeads.length; i++) {
                if (summary[0].search($scope.omittedLeads[i]) >= 0 && summary.length > 1) {
                    summary.shift();
                }
            }

            $uibModalInstance.possibleFilename = summary[0].split("/").pop();

            return [summary, $uibModalInstance.possibleFilename];
        };

        $uibModalInstance.parsedSummary = $uibModalInstance.parseSummary(summary);
        $scope.modalSummary = "Intermittent " + $uibModalInstance.parsedSummary[0].join(" | ");

        $scope.toggleFilerSummaryVisibility = function() {
            $scope.isFilerSummaryVisible = !$scope.isFilerSummaryVisible;
        };

        $scope.isFilerSummaryVisible = false;

        /*
         *  Attempt to find a good product/component for this failure
         */
        $scope.findProduct = function() {
            $scope.suggestedProducts = [];
            var failurePath = $uibModalInstance.parsedSummary[0][0];

            // If the "TEST-UNEXPECTED-foo" isn't one of the omitted ones, use the next piece in the summary
            if (failurePath.includes("TEST-UNEXPECTED-")) {
                failurePath = $uibModalInstance.parsedSummary[0][1];
            }
            var failurePathRoot = failurePath.split("/")[0];

            // Look up the product via the root of the failure's file path
            if (thBugzillaProductObject[failurePathRoot]) {
                $scope.suggestedProducts.push(thBugzillaProductObject[failurePathRoot][0]);
            }

            // Some job types are special, lets explicitly handle them.
            if (selectedJob.job_group_name.includes("Web Platform")) {
                $scope.suggestedProducts.push("Testing :: web-platform-tests");
            }

            // Look up product suggestions via Bugzilla's api
            var productSearch = $scope.productSearch;

            if (productSearch) {
                $http.get("https://bugzilla.mozilla.org/rest/prod_comp_search/" + productSearch + "?limit=5").then(function(request) {
                    var data = request.data;
                    // We can't file unless product and component are provided, this api can return just product. Cut those out.
                    for (var i = data.products.length - 1; i >= 0; i--) {
                        if (!data.products[i].component) {
                            data.products.splice(i, 1);
                        }
                    }
                    $scope.suggestedProducts = [];
                    $scope.suggestedProducts = _.map(data.products, function(prod) {
                        if (prod.product && prod.component) {
                            return prod.product + " :: " + prod.component;
                        }
                        return prod.product;
                    });
                    $scope.selection.selectedProduct = $scope.suggestedProducts[0];
                });
            }

            $scope.selection.selectedProduct = $scope.suggestedProducts[0];
        };

        /*
         *  Same as clicking outside of the modal, but with a nice button-clicking feel...
         */
        $scope.cancelFiler = function() {
            $uibModalInstance.dismiss('cancel');
        };

        $scope.checkedLogLinks = {
            parsedLog: $scope.parsedLog,
            fullLog: $scope.fullLog,
            reftest: $scope.reftest
        };

        $scope.isIntermittent = true;

        /*
         *  Actually send the gathered information to bugzilla.
         */
        $scope.submitFiler = function() {
            var summarystring = $scope.modalSummary;
            var productString = "";
            var componentString = "";

            $scope.toggleForm(true);

            if ($scope.modalSummary.length > 255) {
                thNotify.send("Please ensure the summary is no more than 255 characters", "danger");
                $scope.toggleForm(false);
                return;
            }

            if ($scope.selection.selectedProduct) {
                var prodParts = $scope.selection.selectedProduct.split(" :: ");
                productString += prodParts[0];
                componentString += prodParts[1];
            } else {
                thNotify.send("Please select (or search and select) a product/component pair to continue", "danger");
                $scope.toggleForm(false);
                return;
            }

            var descriptionStrings = _.reduce($scope.checkedLogLinks, function(result, link) {
                if (link) {
                    result = result + link + "\n\n";
                }
                return result;
            }, "");
            if ($scope.modalComment) {
                descriptionStrings += $scope.modalComment;
            }

            var keywords = $scope.isIntermittent ? "intermittent-failure" : "";

            var blocks = $scope.modalBlocks;
            var dependsOn = $scope.modalDependsOn;
            var seeAlso = $scope.modalSeeAlso;

            // Fetch product information from bugzilla to get version numbers, then submit the new bug
            // Only request the versions because some products take quite a long time to fetch the full object
            $.ajax("https://bugzilla.mozilla.org/rest/product/" + productString + "?include_fields=versions").done(function(productJSON) {
                var productObject = productJSON.products[0];

                // Find the newest version for the product that is_active
                var version = _.findLast(productObject.versions, function(version) {
                    return version.is_active === true;
                });

                $http({
                    url: "api/bugzilla/create_bug/",
                    method: "POST",
                    data: {
                        "product": productString,
                        "component": componentString,
                        "summary": summarystring,
                        "keywords": keywords,
                        "version": version.name,
                        "blocks": blocks,
                        "depends_on": dependsOn,
                        "see_also": seeAlso,
                        "comment": descriptionStrings,
                        "comment_tags": "treeherder"
                    }
                }).then(function successCallback(json) {
                    if (json.data.failure) {
                        var errorString = "";
                        for (var i = 0; i < json.data.failure.length; i++) {
                            errorString += json.data.failure[i];
                        }
                        errorString = JSON.parse(errorString);
                        thNotify.send("Bugzilla error: " + errorString.message, "danger", true);
                        $scope.toggleForm(false);
                    } else {
                        // Auto-classify this failure now that the bug has been filed and we have a bug number
                        thPinboard.addBug({id:json.data.success});
                        $rootScope.$evalAsync($rootScope.$emit(thEvents.saveClassification));

                        // Open the newly filed bug in a new tab or window for further editing
                        window.open("https://bugzilla.mozilla.org/show_bug.cgi?id=" + json.data.success);
                        $scope.cancelFiler();
                    }
                }, function errorCallback(response) {
                    var failureString = "Bug Filer API returned status " + response.status + " (" + response.statusText + ")";
                    if (response.data && response.data.failure) {
                        failureString += "\n\n" + response.data.failure;
                    }
                    if (response.status === 403) {
                        failureString += "\n\nAuthentication failed. Has your Treeherder session expired?";
                    }
                    thNotify.send(failureString, "danger");
                    $scope.toggleForm(false);
                });
            });
        };

        /*
         *  Disable or enable form elements as needed at various points in the submission process
         */
        $scope.toggleForm = function(disabled) {
            $(':input','#modalForm').attr("disabled", disabled);
        };
    }
]);

<|endoftext|>


<commit_before>// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

frappe.ui.form.NewTimeline = class {
	constructor(opts) {
		Object.assign(this, opts);
		this.make();
	}

	make() {
		this.timeline_wrapper = $(`<div class="new-timeline">`);
		this.parent.replaceWith(this.timeline_wrapper);
		this.timeline_items = [];
		this.render_timeline_items();
	}

	refresh() {

	}

	add_action_button() {

	}

	render_timeline_items() {
		this.prepare_timeline_contents();

		this.timeline_items.sort((item1, item2) => item1.creation - item2.creation);
		this.timeline_items.forEach(item => {
			this.timeline_wrapper.append(this.get_timeline_item(item));
		});
	}

	prepare_timeline_contents() {
		const doc_info = this.frm.get_docinfo();
		doc_info.views && doc_info.views.forEach(view => {
			this.timeline_items.push({
				icon: 'view',
				creation: view.creation,
				content: this.get_view_content(view),
			});
		});

		Array.from(Array(5)).forEach(() => {
			this.timeline_items.push({
				icon: ['mail', 'view', 'call', 'edit'][Math.floor(Math.random() * 4)],
				creation: Date(),
				content: `Lorem Ipsum is simply dummy text of the printing and
					typesetting industry. Lorem Ipsum has been the industry's
					standard dummy text ever since the 1500s,`,
				card: true
			});
		});

		doc_info.communications &&
			doc_info.communications.forEach(communication => {
				this.timeline_items.push({
					icon: 'mail',
					creation: communication.creation,
					card: true,
					content: this.get_communication_content(communication),
				});
			});
	}

	get_timeline_item(item) {
		const timeline_item = $(`

					${frappe.utils.icon(item.icon, 'md')}

		`);
		timeline_item.find('.timeline-content').append(item.content);
		return timeline_item;
	}

	get_view_content(doc) {
		return `

					${__("{0} viewed", [frappe.user.full_name(doc.owner).bold()])}

				- ${comment_when(doc.creation)}

		`;
	}

	get_communication_content(doc) {
		let item =  $(frappe.render_template('timeline_email', { doc }));
		this.setup_reply(item);
		item.find(".timeline-email-content").append(doc.content);
		return item;
	}

	setup_reply(communication_box) {
		let actions = communication_box.find('.actions');
		let reply = $(`<a class="reply">${frappe.utils.icon('reply', 'md')}</a>`).click(e => {
			console.log(e);
		});
		let reply_all = $(`<a class="reply-all">${frappe.utils.icon('reply-all', 'md')}</a>`).click(e => {
			console.log(e);
		});
		actions.append(reply);
		actions.append(reply_all);
	}
};<commit_msg>feat: Add more timeline contents<commit_after>// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

frappe.ui.form.NewTimeline = class {
	constructor(opts) {
		Object.assign(this, opts);
		this.doc_info = this.frm && this.frm.get_docinfo() || {};
		this.make();
	}

	make() {
		this.timeline_wrapper = $(`<div class="new-timeline">`);
		this.parent.replaceWith(this.timeline_wrapper);
		this.timeline_items = [];
		this.render_timeline_items();
	}

	refresh() {

	}

	add_action_button() {

	}

	render_timeline_items() {
		this.prepare_timeline_contents();

		this.timeline_items.sort((item1, item2) => new Date(item2.creation) - new Date(item1.creation));
		this.timeline_items.forEach(item => {
			this.timeline_wrapper.append(this.get_timeline_item(item));
		});
	}

	prepare_timeline_contents() {
		this.timeline_items.push(...this.get_view_timeline_contents());
		this.timeline_items.push(...this.get_communication_timeline_contents());
		this.timeline_items.push(...this.get_comment_timeline_contents());
		this.timeline_items.push(...this.get_energy_point_timeline_contents());
		// shared
		// milestones
		// versions
		// attachments
	}

	get_timeline_item(item) {
		const timeline_item = $(`

					${frappe.utils.icon(item.icon, 'md')}

		`);
		timeline_item.find('.timeline-content').append(item.content);
		if (!item.hide_timestamp) {
			timeline_item.find('.timeline-content').append(`<div>${comment_when(item.creation)}</div>`);
		}
		return timeline_item;
	}

	get_view_timeline_contents() {
		let view_timeline_contents = [];
		(this.doc_info.views || []).forEach(view => {
			let view_content = `

						${__("{0} viewed", [frappe.user.full_name(view.owner).bold()])}

			`;
			view_timeline_contents.push({
				icon: 'view',
				creation: view.creation,
				content: view_content,
			});
		});
		return view_timeline_contents;
	}

	get_communication_timeline_contents() {
		let communication_timeline_contents = [];
		(this.doc_info.communications|| []).forEach(communication => {
			let communication_content =  $(frappe.render_template('timeline_email', { doc: communication }));
			this.setup_reply(communication_content);
			communication_content.find(".timeline-email-content").append(communication.content);
			communication_timeline_contents.push({
				icon: 'mail',
				creation: communication.creation,
				card: true,
				content: this.get_communication_content(communication),
			});
		});
		return communication_timeline_contents;
	}

	get_comment_timeline_contents() {
		let comment_timeline_contents = [];
		(this.doc_info.comments || []).forEach(comment => {
			comment_timeline_contents.push({
				icon: 'mail',
				creation: comment.creation,
				content: comment.content,
			});
		});
		return comment_timeline_contents;
	}

	get_version_timeline_contents() {
		let version_timeline_contents = [];
		(this.doc_info.versions || []).forEach(version => {
			version_timeline_contents.push({
				icon: 'mail',
				creation: version.creation,
				content: version.content,
			});
		});
		return version_timeline_contents;
	}

	get_energy_point_timeline_contents() {
		let energy_point_timeline_contents = [];
		(this.doc_info.energy_point_logs || []).forEach(log => {
			energy_point_timeline_contents.push({
				icon: 'review',
				creation: log.creation,
				content: frappe.energy_points.format_form_log(log)
			});
		});
		return energy_point_timeline_contents;
	}

	setup_reply(communication_box) {
		let actions = communication_box.find('.actions');
		let reply = $(`<a class="reply">${frappe.utils.icon('reply', 'md')}</a>`).click(e => {
			console.log(e);
		});
		let reply_all = $(`<a class="reply-all">${frappe.utils.icon('reply-all', 'md')}</a>`).click(e => {
			console.log(e);
		});
		actions.append(reply);
		actions.append(reply_all);
	}
};<|endoftext|>


<commit_before>(function () {
  'use strict';

  angular
    .module('support')
    .controller('SupportController', SupportController);

  /* @ngInject */
  function SupportController(SupportService, messageCenterService, $stateParams) {

    // ViewModel
    var vm = this;

    // Expoxed to the view
    vm.sendSupportRequest = sendSupportRequest;
    vm.success = false;
    vm.isLoading = false;
    vm.request = {
      username: '',
      email: '',
      message: ''
    };

    activate();

    /**
     * Initialize controller
     */
    function activate() {
      if ($stateParams.report && $stateParams.report !== '') {
        vm.request.reportMember = $stateParams.report;
      }
    }

    /**
     * Send support request
     */
    function sendSupportRequest(isValid) {
      vm.success = false;
      vm.isLoading = true;

      if (!isValid) {
        vm.isLoading = false;
        return false;
      }

      if (vm.request.message === '') {
        messageCenterService.add('danger', 'Please write a message first.', { timeout: 20000 });
        vm.isLoading = false;
        return false;
      }

      var supportRequest = new SupportService(vm.request);

      supportRequest.$save(function () {
        vm.success = true;
        vm.isLoading = false;
      }, function (err) {
        vm.isLoading = false;
        messageCenterService.add('danger', err.message || 'Something went wrong. Please try again.', { timeout: 20000 });
      });

    }

  }

}());
<commit_msg>fix typo in comment<commit_after>(function () {
  'use strict';

  angular
    .module('support')
    .controller('SupportController', SupportController);

  /* @ngInject */
  function SupportController(SupportService, messageCenterService, $stateParams) {

    // ViewModel
    var vm = this;

    // Exposed to the view
    vm.sendSupportRequest = sendSupportRequest;
    vm.success = false;
    vm.isLoading = false;
    vm.request = {
      username: '',
      email: '',
      message: ''
    };

    activate();

    /**
     * Initialize controller
     */
    function activate() {
      if ($stateParams.report && $stateParams.report !== '') {
        vm.request.reportMember = $stateParams.report;
      }
    }

    /**
     * Send support request
     */
    function sendSupportRequest(isValid) {
      vm.success = false;
      vm.isLoading = true;

      if (!isValid) {
        vm.isLoading = false;
        return false;
      }

      if (vm.request.message === '') {
        messageCenterService.add('danger', 'Please write a message first.', { timeout: 20000 });
        vm.isLoading = false;
        return false;
      }

      var supportRequest = new SupportService(vm.request);

      supportRequest.$save(function () {
        vm.success = true;
        vm.isLoading = false;
      }, function (err) {
        vm.isLoading = false;
        messageCenterService.add('danger', err.message || 'Something went wrong. Please try again.', { timeout: 20000 });
      });

    }

  }

}());
<|endoftext|>


<commit_before>var debug = require('debug')('holodex:agent:model')
var _ = require('lodash')

var Model = require('base/model')
var Relationships = require('relationship/collection')
var relationships = require('relationship/store')
var Roles = require('role/collection')
var roles = require('role/store')

module.exports = Model.extend({
  typeName: 'Agent',
  collectionName: 'agents',

  props: {
    name: 'string',
    image: 'url'
  },

  initialize: function (attrs, opts) {
    debug('initialize', this)
    Model.prototype.initialize.apply(this, arguments)

    this.roles = new Roles(roles, {
      where: {
        agent: this
      }
    })

    this.relationships = new Relationships(relationships, {
      filter: function (relationship) {
        return relationship.roles.some(function (role) {
          role.agent === this
        }, this)
      }.bind(this)
    })
  },

  derived: {
    relsByType: {
      deps: ['relationships'],
      cache: false,
      fn: function () {
        debug('relationships', this.relationships)

        var rels = {}
        this.relationships.forEach(function (rel) {
          var relType = rel.type
          if (!relType) return
          var relType = relType.getId()
          if (!rels[relType]) {
            rels[relType] = new Relationships()
          }
          rels[relType].add(rel)
        })

        debug('relsByType ->', rels)
        return rels
      },
    },

    relatedTypes: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        var relatedTypes = Object.keys(this.relsByType)
        debug('relatedTypes ->', relatedTypes)
        return relatedTypes
      },
    },

    relatedNodes: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        var relatedNodes = _.union(_.flatten(
          this.relationships.map(function (rel) {
            return [rel.is].concat([rel.has])
          })
        ))
        debug('relatedNodes ->', relatedNodes)
        return relatedNodes
      },
    },

    relatedEdges: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        var relatedEdges = {}

        _.forEach(this.relatedTypes, function (relType) {
          relatedEdges[relType] = {}
        })

        this.relationships.forEach(function (rel) {
          var relType = rel.type
          if (!relType) return
          relType = relType.getId()

          var relEdges = relatedEdges[relType]

          var relIs = rel.is.getId()
          var relHas = rel.has.getId()

          if (!_.isArray(relEdges[relIs])) {
            relEdges[relIs] = []
          }
          relEdges[relIs].push(relHas)
        })

        debug('relatedEdges ->', relatedEdges)
        return relatedEdges
      },
    },

    // TODO inward and outward relationships
    hasRelsByType: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        debug('relsByType', this.relsByType)

        var hasRels = {}

        _.forEach(this.relsByType, function (rels, isType) {

          var hasType = isType.symmetric

          hasRels[hasType] = new Relationships()

          rels.forEach(function (rel) {

            if (
              rel.is &&
              (rel.is.getId() === this.getId())
            ) {
              var is = rel.is
              var has = is.symmetric
              hasRels[hasType].add(has)
            }

          }, this)
        }, this)

        debug('hasRelsByType ->', hasRels)
        return hasRels
      },
    },
  },

})
<commit_msg>fix agent roles view<commit_after>var debug = require('debug')('holodex:agent:model')
var _ = require('lodash')

var Model = require('base/model')
var Relationships = require('relationship/collection')
var relationships = require('relationship/store')
var Roles = require('role/collection')
var roles = require('role/store')

module.exports = Model.extend({
  typeName: 'Agent',
  collectionName: 'agents',

  props: {
    name: 'string',
    image: 'url'
  },

  initialize: function (attrs, opts) {
    debug('initialize', this)
    Model.prototype.initialize.apply(this, arguments)

    this.roles = new Roles(roles, {
      where: {
        agent: this
      }
    })

    this.relationships = new Relationships(relationships, {
      filter: function (relationship) {
        if (!relationship.roles) return
        return relationship.roles.some(function (role) {
          role.agent === this
        }, this)
      }.bind(this)
    })
  },

  derived: {
    relsByType: {
      deps: ['relationships'],
      cache: false,
      fn: function () {
        debug('relationships', this.relationships)

        var rels = {}
        this.relationships.forEach(function (rel) {
          var relType = rel.type
          if (!relType) return
          var relType = relType.getId()
          if (!rels[relType]) {
            rels[relType] = new Relationships()
          }
          rels[relType].add(rel)
        })

        debug('relsByType ->', rels)
        return rels
      },
    },

    relatedTypes: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        var relatedTypes = Object.keys(this.relsByType)
        debug('relatedTypes ->', relatedTypes)
        return relatedTypes
      },
    },

    relatedNodes: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        var relatedNodes = _.union(_.flatten(
          this.relationships.map(function (rel) {
            return [rel.is].concat([rel.has])
          })
        ))
        debug('relatedNodes ->', relatedNodes)
        return relatedNodes
      },
    },

    relatedEdges: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        var relatedEdges = {}

        _.forEach(this.relatedTypes, function (relType) {
          relatedEdges[relType] = {}
        })

        this.relationships.forEach(function (rel) {
          var relType = rel.type
          if (!relType) return
          relType = relType.getId()

          var relEdges = relatedEdges[relType]

          var relIs = rel.is.getId()
          var relHas = rel.has.getId()

          if (!_.isArray(relEdges[relIs])) {
            relEdges[relIs] = []
          }
          relEdges[relIs].push(relHas)
        })

        debug('relatedEdges ->', relatedEdges)
        return relatedEdges
      },
    },

    // TODO inward and outward relationships
    hasRelsByType: {
      deps: ['relsByType'],
      cache: false,
      fn: function () {
        debug('relsByType', this.relsByType)

        var hasRels = {}

        _.forEach(this.relsByType, function (rels, isType) {

          var hasType = isType.symmetric

          hasRels[hasType] = new Relationships()

          rels.forEach(function (rel) {

            if (
              rel.is &&
              (rel.is.getId() === this.getId())
            ) {
              var is = rel.is
              var has = is.symmetric
              hasRels[hasType].add(has)
            }

          }, this)
        }, this)

        debug('hasRelsByType ->', hasRels)
        return hasRels
      },
    },
  },

})
<|endoftext|>


<commit_before>/*
 * grunt-imageoptim
 * https://github.com/JamieMason/grunt-imageoptim
 * Copyright © 2013 Jamie Mason, @GotNoSugarBaby,
 * https://github.com/JamieMason
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

module.exports = function(grunt) {

  'use strict';

  var q = require('q');
  var path = require('path');
  var childProcess = require('child_process');
  var exec = childProcess.exec;
  var spawn = childProcess.spawn;
  var gruntFile = path.resolve();
  var taskName = 'imageoptim';
  var issuesPage = 'https://github.com/JamieMason/grunt-imageoptim/issues/new';
  var taskDescription = 'Losslessly compress images from the command line';
  var cliPaths = [
    '../node_modules/imageoptim-cli/bin',
    '../../imageoptim-cli/bin'
  ].map(function(dir) {
    return path.resolve(__dirname, dir);
  });

  (function() {
    var config = grunt.config.data.imageoptim || {};
    var tasks = config ? [config] : [];
    var hasDeprecatedConfig = false;
    var error = '';

    Object.keys(config).forEach(function(key) {
      var value = config[key];
      var isObject = value && typeof value === 'object';
      var isTask = isObject && key !== 'options' && key !== 'files';
      if (isTask) {
        tasks.push(value);
      }
    });

    hasDeprecatedConfig = tasks.some(function(task) {
      return 'files' in task && task.files.some(function(member) {
        return typeof member === 'string';
      });
    });

    if (hasDeprecatedConfig) {
      error += '\n';
      error += 'grunt-imageoptim 1.4.0 brought a change to it\'s configuration to bring full ';
      error += 'support for Grunt\'s file pattern matching.';
      error += '\n';
      error += 'In most cases all this means is renaming the "files" property to "src", but ';
      error += 'updated examples can be found at https://github.com/JamieMason/grunt-imageoptim.';
      grunt.fail.fatal(error);
    }
  }());

  /**
   * Get the ImageOptim-CLI Terminal command to be run for a given directory and task options
   * @param  {String} directory
   * @param  {Object} options
   * @return {String}
   */

  function getCommandByDirectory(directory, options) {
    var command = './imageOptim';
    command += options.quitAfter ? ' --quit' : '';
    command += options.imageAlpha ? ' --image-alpha' : '';
    command += options.jpegMini ? ' --jpeg-mini' : '';
    return command + ' --directory ' + directory.replace(/\s/g, '\\ ');
  }

  /**
   * @param  {String} command
   * @param  {String} cwd
   * @return {Promise}
   */

  function executeDirectoryCommand(command, cwd) {

    var deferred = q.defer();
    var errorMessage = 'ImageOptim-CLI exited with a failure status';
    var imageOptimCli = exec(command, {
      cwd: cwd
    });

    imageOptimCli.stdout.on('data', function(message) {
      console.log(String(message || '').replace(/\n+$/, ''));
    });

    imageOptimCli.on('exit', function(code) {
      return code === 0 ? deferred.resolve(true) : deferred.reject(new Error(errorMessage));
    });

    return deferred.promise;

  }

  /**
   * @param  {String[]}  files             Array of paths to directories from src: in config.
   * @param  {Boolean}   opts.jpegMini     Whether to run JPEGmini.app.
   * @param  {Boolean}   opts.imageAlpha   Whether to run ImageAlpha.app.
   * @param  {Boolean}   opts.quitAfter    Whether to quit apps after running.
   * @return {Promise}
   */

  function processDirectories(files, opts) {
    return files.map(function(directory) {
      return getCommandByDirectory(directory, opts);
    }).reduce(function(promise, command) {
      return promise.then(function() {
        return executeDirectoryCommand(command, opts.cliPath);
      });
    }, q());
  }

  /**
   * include necessary command line flags based on the current task's options
   * @param  {Object} opts
   * @return {Array}
   */

  function getCliFlags(opts) {
    var cliOptions = [];
    if (opts.quitAfter) {
      cliOptions.push('--quit');
    }
    if (opts.imageAlpha) {
      cliOptions.push('--image-alpha');
    }
    if (opts.jpegMini) {
      cliOptions.push('--jpeg-mini');
    }
    return cliOptions;
  }

  /**
   * @param  {String[]}  files             Array of paths to files from src: in config.
   * @param  {Boolean}   opts.jpegMini     Whether to run JPEGmini.app.
   * @param  {Boolean}   opts.imageAlpha   Whether to run ImageAlpha.app.
   * @param  {Boolean}   opts.quitAfter    Whether to quit apps after running.
   * @return {Promise}
   */

  function processFiles(files, opts) {

    var imageOptimCli;
    var deferred = q.defer();
    var errorMessage = 'ImageOptim-CLI exited with a failure status';

    imageOptimCli = spawn('./imageOptim', getCliFlags(opts), {
      cwd: opts.cliPath
    });

    imageOptimCli.stdout.on('data', function(message) {
      console.log(String(message || '').replace(/\n+$/, ''));
    });

    imageOptimCli.on('exit', function(code) {
      return code === 0 ? deferred.resolve(true) : deferred.reject(new Error(errorMessage));
    });

    imageOptimCli.stdin.setEncoding('utf8');
    imageOptimCli.stdin.end(files.join('\n'));

    return deferred.promise;

  }

  /**
   * @param  {String} str "hello"
   * @return {String}     "Hello"
   */

  function firstUp(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  }

  /**
   * @param  {String}  fileType "file" or "Dir"
   * @return {Function}
   */

  function isFileType(fileType) {
    var methodName = 'is' + firstUp(fileType);
    return function(file) {
      return grunt.file[methodName](file);
    };
  }

  /**
   * Ensure the ImageOptim-CLI binary is accessible
   * @return {String}
   */

  function getPathToCli() {
    return cliPaths.filter(function(cliPath) {
      return grunt.file.exists(cliPath);
    })[0];
  }

  /**
   * Convert a relative path to an absolute file system path
   * @param  {String} relativePath
   * @return {String}
   */

  function toAbsolute(relativePath) {
    return path.resolve(gruntFile, relativePath);
  }

  /**
   * Given a collection of files to be run in a task, seperate the files from the directories to
   * handle them in their own way.
   * @param  {String} fileType "dir" or "file"
   * @param  {String} cliPath
   * @param  {Object} options
   * @param  {Array} taskFiles
   * @param  {Promise} promise
   * @return {Promise}
   */

  function processBatch(fileType, cliPath, options, taskFiles, promise) {
    var files = taskFiles.filter(isFileType(fileType)).map(toAbsolute);
    var processor = fileType === 'dir' ? processDirectories : processFiles;
    return files.length === 0 ? promise : promise.then(function() {
      return processor(files, {
        cliPath: cliPath,
        jpegMini: options.jpegMini,
        imageAlpha: options.imageAlpha,
        quitAfter: options.quitAfter
      });
    });
  }

  grunt.registerMultiTask(taskName, taskDescription, function() {

    var task = this;
    var done = task.async();
    var promise = q();
    var cliPath = getPathToCli();
    var options = task.options({
      jpegMini: false,
      imageAlpha: false,
      quitAfter: false
    });

    if (!cliPath) {
      throw new Error('Unable to locate ImageOptim-CLI. Please raise issue at ' + issuesPage);
    }

    task.files.forEach(function(file) {
      promise = processBatch('file', cliPath, options, file.src, promise);
      promise = processBatch('dir', cliPath, options, file.src, promise);
    });

    promise.done(done);

  });

};
<commit_msg>Fix missing final STDIN newline when spawning ImageOptim<commit_after>/*
 * grunt-imageoptim
 * https://github.com/JamieMason/grunt-imageoptim
 * Copyright © 2013 Jamie Mason, @GotNoSugarBaby,
 * https://github.com/JamieMason
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

module.exports = function(grunt) {

  'use strict';

  var q = require('q');
  var path = require('path');
  var childProcess = require('child_process');
  var exec = childProcess.exec;
  var spawn = childProcess.spawn;
  var gruntFile = path.resolve();
  var taskName = 'imageoptim';
  var issuesPage = 'https://github.com/JamieMason/grunt-imageoptim/issues/new';
  var taskDescription = 'Losslessly compress images from the command line';
  var cliPaths = [
    '../node_modules/imageoptim-cli/bin',
    '../../imageoptim-cli/bin'
  ].map(function(dir) {
    return path.resolve(__dirname, dir);
  });

  (function() {
    var config = grunt.config.data.imageoptim || {};
    var tasks = config ? [config] : [];
    var hasDeprecatedConfig = false;
    var error = '';

    Object.keys(config).forEach(function(key) {
      var value = config[key];
      var isObject = value && typeof value === 'object';
      var isTask = isObject && key !== 'options' && key !== 'files';
      if (isTask) {
        tasks.push(value);
      }
    });

    hasDeprecatedConfig = tasks.some(function(task) {
      return 'files' in task && task.files.some(function(member) {
        return typeof member === 'string';
      });
    });

    if (hasDeprecatedConfig) {
      error += '\n';
      error += 'grunt-imageoptim 1.4.0 brought a change to it\'s configuration to bring full ';
      error += 'support for Grunt\'s file pattern matching.';
      error += '\n';
      error += 'In most cases all this means is renaming the "files" property to "src", but ';
      error += 'updated examples can be found at https://github.com/JamieMason/grunt-imageoptim.';
      grunt.fail.fatal(error);
    }
  }());

  /**
   * Get the ImageOptim-CLI Terminal command to be run for a given directory and task options
   * @param  {String} directory
   * @param  {Object} options
   * @return {String}
   */

  function getCommandByDirectory(directory, options) {
    var command = './imageOptim';
    command += options.quitAfter ? ' --quit' : '';
    command += options.imageAlpha ? ' --image-alpha' : '';
    command += options.jpegMini ? ' --jpeg-mini' : '';
    return command + ' --directory ' + directory.replace(/\s/g, '\\ ');
  }

  /**
   * @param  {String} command
   * @param  {String} cwd
   * @return {Promise}
   */

  function executeDirectoryCommand(command, cwd) {

    var deferred = q.defer();
    var errorMessage = 'ImageOptim-CLI exited with a failure status';
    var imageOptimCli = exec(command, {
      cwd: cwd
    });

    imageOptimCli.stdout.on('data', function(message) {
      console.log(String(message || '').replace(/\n+$/, ''));
    });

    imageOptimCli.on('exit', function(code) {
      return code === 0 ? deferred.resolve(true) : deferred.reject(new Error(errorMessage));
    });

    return deferred.promise;

  }

  /**
   * @param  {String[]}  files             Array of paths to directories from src: in config.
   * @param  {Boolean}   opts.jpegMini     Whether to run JPEGmini.app.
   * @param  {Boolean}   opts.imageAlpha   Whether to run ImageAlpha.app.
   * @param  {Boolean}   opts.quitAfter    Whether to quit apps after running.
   * @return {Promise}
   */

  function processDirectories(files, opts) {
    return files.map(function(directory) {
      return getCommandByDirectory(directory, opts);
    }).reduce(function(promise, command) {
      return promise.then(function() {
        return executeDirectoryCommand(command, opts.cliPath);
      });
    }, q());
  }

  /**
   * include necessary command line flags based on the current task's options
   * @param  {Object} opts
   * @return {Array}
   */

  function getCliFlags(opts) {
    var cliOptions = [];
    if (opts.quitAfter) {
      cliOptions.push('--quit');
    }
    if (opts.imageAlpha) {
      cliOptions.push('--image-alpha');
    }
    if (opts.jpegMini) {
      cliOptions.push('--jpeg-mini');
    }
    return cliOptions;
  }

  /**
   * @param  {String[]}  files             Array of paths to files from src: in config.
   * @param  {Boolean}   opts.jpegMini     Whether to run JPEGmini.app.
   * @param  {Boolean}   opts.imageAlpha   Whether to run ImageAlpha.app.
   * @param  {Boolean}   opts.quitAfter    Whether to quit apps after running.
   * @return {Promise}
   */

  function processFiles(files, opts) {

    var imageOptimCli;
    var deferred = q.defer();
    var errorMessage = 'ImageOptim-CLI exited with a failure status';

    imageOptimCli = spawn('./imageOptim', getCliFlags(opts), {
      cwd: opts.cliPath
    });

    imageOptimCli.stdout.on('data', function(message) {
      console.log(String(message || '').replace(/\n+$/, ''));
    });

    imageOptimCli.on('exit', function(code) {
      return code === 0 ? deferred.resolve(true) : deferred.reject(new Error(errorMessage));
    });

    imageOptimCli.stdin.setEncoding('utf8');
    imageOptimCli.stdin.end(files.join('\n') + '\n');

    return deferred.promise;

  }

  /**
   * @param  {String} str "hello"
   * @return {String}     "Hello"
   */

  function firstUp(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  }

  /**
   * @param  {String}  fileType "file" or "Dir"
   * @return {Function}
   */

  function isFileType(fileType) {
    var methodName = 'is' + firstUp(fileType);
    return function(file) {
      return grunt.file[methodName](file);
    };
  }

  /**
   * Ensure the ImageOptim-CLI binary is accessible
   * @return {String}
   */

  function getPathToCli() {
    return cliPaths.filter(function(cliPath) {
      return grunt.file.exists(cliPath);
    })[0];
  }

  /**
   * Convert a relative path to an absolute file system path
   * @param  {String} relativePath
   * @return {String}
   */

  function toAbsolute(relativePath) {
    return path.resolve(gruntFile, relativePath);
  }

  /**
   * Given a collection of files to be run in a task, seperate the files from the directories to
   * handle them in their own way.
   * @param  {String} fileType "dir" or "file"
   * @param  {String} cliPath
   * @param  {Object} options
   * @param  {Array} taskFiles
   * @param  {Promise} promise
   * @return {Promise}
   */

  function processBatch(fileType, cliPath, options, taskFiles, promise) {
    var files = taskFiles.filter(isFileType(fileType)).map(toAbsolute);
    var processor = fileType === 'dir' ? processDirectories : processFiles;
    return files.length === 0 ? promise : promise.then(function() {
      return processor(files, {
        cliPath: cliPath,
        jpegMini: options.jpegMini,
        imageAlpha: options.imageAlpha,
        quitAfter: options.quitAfter
      });
    });
  }

  grunt.registerMultiTask(taskName, taskDescription, function() {

    var task = this;
    var done = task.async();
    var promise = q();
    var cliPath = getPathToCli();
    var options = task.options({
      jpegMini: false,
      imageAlpha: false,
      quitAfter: false
    });

    if (!cliPath) {
      throw new Error('Unable to locate ImageOptim-CLI. Please raise issue at ' + issuesPage);
    }

    task.files.forEach(function(file) {
      promise = processBatch('file', cliPath, options, file.src, promise);
      promise = processBatch('dir', cliPath, options, file.src, promise);
    });

    promise.done(done);

  });

};
<|endoftext|>