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

/**
 * Model element that represents a burner in the simulation.  The burner can heat and also cool other model elements.
 *
 * @author John Blanco
 * @author Jesse Greenberg
 * @author Andrew Adare
 */

define( function( require ) {
  'use strict';

  // modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var EnergyChunk = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunk' );
  var EnergyChunkWanderController = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunkWanderController' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  var HorizontalSurface = require( 'ENERGY_FORMS_AND_CHANGES/common/model/HorizontalSurface' );
  var inherit = require( 'PHET_CORE/inherit' );
  var ModelElement = require( 'ENERGY_FORMS_AND_CHANGES/common/model/ModelElement' );
  var ObservableArray = require( 'AXON/ObservableArray' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Rectangle = require( 'DOT/Rectangle' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var SIDE_LENGTH = 0.075; // in meters
  var MAX_ENERGY_GENERATION_RATE = 5000; // joules/sec, empirically chosen
  var CONTACT_DISTANCE = 0.001; // in meters
  var ENERGY_CHUNK_CAPTURE_DISTANCE = 0.2; // in meters, empirically chosen

  // Because of the way that energy chunks are exchanged between thermal modeling elements within this simulation,
  // things can end up looking a bit odd if a burner is turned on with nothing on it.  To account for this, a separate
  // energy generation rate is used when a burner is exchanging energy directly with the air.
  // Units: joules/sec, multiplier empirically chosen.
  var MAX_ENERGY_GENERATION_RATE_INTO_AIR = MAX_ENERGY_GENERATION_RATE * 0.3;

  // counter used by constructor to create unique IDs for each burner
  var idCounter = 0;

  /**
   * @param {Vector2} position - the position in model space where this burner exists
   * @param {Property.<boolean>} energyChunksVisibleProperty - controls whether the energy chunks are visible
   * @constructor
   */
  function Burner( position, energyChunksVisibleProperty ) {

    var self = this;
    ModelElement.call( this, position );

    // @public (read-only) {string} - unique ID, used for debug
    this.id = 'burner-' + idCounter++;

    // @public {Property<number>}
    this.heatCoolLevelProperty = new Property( 0 );

    // @public (read-only) {ObservableArray.<EnergyChunk>}
    this.energyChunkList = new ObservableArray();

    // @private {Vector2}
    this.position = position;

    // @private {Object[]} - motion strategies that control the movement of the energy chunks owned by this burner
    this.energyChunkMotionStrategies = [];

    // @private {Range} - used to keep incoming energy chunks from wandering very far to the left or right
    this.incomingEnergyChunkWanderBounds = new Range( position.x - SIDE_LENGTH / 4, position.x + SIDE_LENGTH / 4 );

    // @private {Property.<boolean>}
    this.energyChunksVisibleProperty = energyChunksVisibleProperty;

    // add position test bounds (see definition in base class for more info)
    this.relativePositionTestingBoundsList.push( new Bounds2( -SIDE_LENGTH / 2, 0, SIDE_LENGTH / 2, SIDE_LENGTH ) );

    // Create and add the top surface.  Some compensation for perspective is necessary in order to avoid problems with
    // edge overlap when dropping objects on top of burner.
    var compositeBounds = self.getCompositeBounds();
    var perspectiveCompensation = compositeBounds.height * EFACConstants.BURNER_EDGE_TO_HEIGHT_RATIO *
                                  Math.cos( EFACConstants.BURNER_PERSPECTIVE_ANGLE ) / 2;

    // @public - see base class for description
    this.topSurface = new HorizontalSurface(
      new Vector2( this.position.x, compositeBounds.maxY ),
      compositeBounds.maxX + perspectiveCompensation - ( compositeBounds.minX - perspectiveCompensation ),
      this
    );
  }

  energyFormsAndChanges.register( 'Burner', Burner );

  return inherit( ModelElement, Burner, {

    /**
     * Get a rectangle that defines the outline of the burner.  In the model the burner is essentially a 2D rectangle.
     * @returns {Rectangle} - rectangle that defines the outline in model space.
     * @public
     */
    getCompositeBounds: function() {
      // TODO: This is wasteful to reconstruct this every time, since burners don't move, should be optimized.  Also should be bounds.
      var minX = this.position.x - SIDE_LENGTH / 2;
      var minY = this.position.y;
      return new Bounds2( minX, minY, minX + SIDE_LENGTH, minY + SIDE_LENGTH
      );
    },

    /**
     * @returns {HorizontalSurface}
     * @public
     */
    getTopSurface: function() {
      return this.topSurface;
    },

    /**
     * Interact with a thermal energy container, adding or removing energy based on the current heat/cool setting.
     * NOTE: this shouldn't be used for air - there is a specific method for that
     * @param {ThermalEnergyContainer} thermalEnergyContainer - model object that will get or give energy
     * @param {number} dt - amount of time (delta time)
     * @returns {number} - amount of energy transferred in joules, can be negative if energy was drawn from object
     * @public
     */
    addOrRemoveEnergyToFromObject: function( thermalEnergyContainer, dt ) {

      var deltaEnergy = 0;

      if ( this.inContactWith( thermalEnergyContainer ) ) {
        if ( thermalEnergyContainer.getTemperature() > EFACConstants.WATER_FREEZING_POINT_TEMPERATURE ) {
          deltaEnergy = MAX_ENERGY_GENERATION_RATE * this.heatCoolLevelProperty.value * dt;

          // make sure we don't cool the object below its minimum allowed energy level
          if ( this.heatCoolLevelProperty.value < 0 ) {
            if ( Math.abs( deltaEnergy ) > thermalEnergyContainer.getEnergyAboveMinimum() ) {
              deltaEnergy = -thermalEnergyContainer.getEnergyAboveMinimum();
            }
          }
        }
        thermalEnergyContainer.changeEnergy( deltaEnergy );
      }
      return deltaEnergy;
    },

    /**
     * Exchange energy with the air.  This has specific behavior that is different from addOrRemoveEnergyToFromObject,
     * defined elsewhere in this type.
     * @param {Air} air - air as a thermal energy container
     * @param dt - amount of time (delta time)
     * @returns {number} - energy, in joules, transferred to the air, negative if energy was absorbed
     * @public
     */
    addOrRemoveEnergyToFromAir: function( air, dt ) {
      var deltaEnergy = MAX_ENERGY_GENERATION_RATE_INTO_AIR * this.heatCoolLevelProperty.value * dt;
      air.changeEnergy( deltaEnergy );
      return deltaEnergy;
    },

    /**
     * determine if the burner is in contact with a thermal energy container
     * @param {ThermalEnergyContainer} thermalEnergyContainer
     * @returns {boolean}
     * @public
     */
    inContactWith: function( thermalEnergyContainer ) {
      var burnerRect = this.getCompositeBounds();
      var area = thermalEnergyContainer.thermalContactArea;
      var xContact = ( area.centerX > burnerRect.minX && area.centerX < burnerRect.maxX );
      var yContact = ( Math.abs( area.minY - burnerRect.maxY ) < CONTACT_DISTANCE );
      return ( xContact && yContact );
    },

    /**
     * @param {EnergyChunk} energyChunk
     * @public
     */
    addEnergyChunk: function( energyChunk ) {

      // make sure the chunk is at the front (which makes it fully opaque in the view)
      energyChunk.zPositionProperty.value = 0;

      // create a motion strategy that will move this energy chunk
      var motionStrategy = new EnergyChunkWanderController(
        energyChunk,
        new Property( this.getCenterPoint() ),
        { horizontalWanderConstraint: this.incomingEnergyChunkWanderBounds, wanderAngleVariation: Math.PI * 0.15 }
      );

      energyChunk.zPosition = 0;

      // add the chunk and its motion strategy to this model
      this.energyChunkList.add( energyChunk );
      this.energyChunkMotionStrategies.push( motionStrategy );
    },

    /**
     * request an energy chunk from the burner
     * @param {Vector2} point - point from which to search for closest chunk
     * @returns {EnergyChunk} - closest energy chunk, null if none are contained
     * @public
     */
    extractEnergyChunkClosestToPoint: function( point ) {
      var self = this;
      var closestEnergyChunk = null;
      if ( this.energyChunkList.length > 0 ) {
        this.energyChunkList.forEach( function( energyChunk ) {
          if ( energyChunk.positionProperty.value.distance( self.position ) > ENERGY_CHUNK_CAPTURE_DISTANCE &&
               ( closestEnergyChunk === null ||
                 energyChunk.positionProperty.value.distance( point ) <
                 closestEnergyChunk.positionProperty.value.distance( point ) ) ) {

            // found a closer chunk
            closestEnergyChunk = energyChunk;
          }
        } );

        this.energyChunkList.remove( closestEnergyChunk );
        this.energyChunkMotionStrategies.forEach( function( energyChunkWanderController, index ) {
          if ( energyChunkWanderController.energyChunk === closestEnergyChunk ) {
            self.energyChunkMotionStrategies.splice( index, 1 );
          }
        } );
      }

      if ( closestEnergyChunk === null && this.heatCoolLevelProperty.value > 0 ) {

        // create an energy chunk
        closestEnergyChunk = new EnergyChunk(
          EnergyType.THERMAL,
          this.getCenterPoint(), // will originate from the center of this burner
          new Vector2( 0, 0 ),
          this.energyChunksVisibleProperty
        );
      }

      if ( closestEnergyChunk === null ) {

        // This probably shouldn't ever occur, but it doesn't really warrant an assert, so log a warning that will
        // encourage investigation if it DOES happen.
        console.warn( 'Burner was unable to supply energy chunks.' );
      }
      return closestEnergyChunk;
    },

    /**
     * request an energy chunk from the burner based on provided bounds
     * @param {Bounds2} bounds - bounds to which the energy chunks will be heading
     * @returns {EnergyChunk} - closest energy chunk
     * @public
     */
    extractEnergyChunkClosestToBounds: function( bounds ) {

      // verify that the bounds where the energy chunk is going are above this burner
      var burnerBounds = this.getCompositeBounds();
      assert && assert(
      bounds.minY >= burnerBounds.maxY && bounds.centerX > burnerBounds.minX && bounds.centerX < burnerBounds.maxX,
        'items should only be on top of burner when getting ECs'
      );
      return this.extractEnergyChunkClosestToPoint( new Vector2( bounds.centerX, bounds.minY ) );
    },

    /**
     * @returns {Vector2}
     * @public
     */
    getCenterPoint: function() {
      return new Vector2( this.position.x, this.position.y + SIDE_LENGTH / 2 );
    },

    /**
     * @public
     */
    reset: function() {
      ModelElement.prototype.reset.call( this );
      this.energyChunkList.clear();
      this.heatCoolLevelProperty.reset();
    },

    /**
     * @param {ThermalEnergyContainer[]} thermalEnergyContainers
     * @returns {boolean}
     * @public
     */
    areAnyOnTop: function( thermalEnergyContainers ) {
      var self = this; // Provide a handle for use in nested callback
      var onTop = false;
      thermalEnergyContainers.forEach( function( thermalEnergyContainer ) {
        if ( self.inContactWith( thermalEnergyContainer ) ) {
          onTop = true;
        }
      } );
      return onTop;
    },

    /**
     * animate the energy chunks
     * @param {number} dt
     * @private
     */
    step: function( dt ) {
      var self = this;
      var controllers = this.energyChunkMotionStrategies.slice();

      controllers.forEach( function( controller ) {

        controller.updatePosition( dt );

        if ( controller.isDestinationReached() ) {
          self.energyChunkList.remove( controller.energyChunk );
          _.pull( self.energyChunkMotionStrategies, controller );
        }
      } );
    },

    /**
     * get a rectangle in model space the corresponds to where the flame and ice exist
     * @returns {Rectangle}
     * @public
     */
    getFlameIceRect: function() {

      // word of warning: this needs to stay consistent with the view
      var outlineRect = this.getCompositeBounds();
      var width = outlineRect.width;
      var height = outlineRect.height;
      return new Rectangle( outlineRect.centerX - width / 4, outlineRect.centerY, width / 2, height / 2 );
    },

    /**
     * get burner temperature, clamped at minimum to the freezing point of water
     * @returns {number}
     * @public
     */
    getTemperature: function() {
      var temperature = EFACConstants.ROOM_TEMPERATURE + this.heatCoolLevelProperty.value * 100;
      return Math.max( temperature, EFACConstants.WATER_FREEZING_POINT_TEMPERATURE );
    }

  } );
} );

<commit_msg>Update TODO, see https://github.com/phetsims/energy-forms-and-changes/issues/183<commit_after>// Copyright 2014-2018, University of Colorado Boulder

/**
 * Model element that represents a burner in the simulation.  The burner can heat and also cool other model elements.
 *
 * @author John Blanco
 * @author Jesse Greenberg
 * @author Andrew Adare
 */

define( function( require ) {
  'use strict';

  // modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var EnergyChunk = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunk' );
  var EnergyChunkWanderController = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunkWanderController' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  var HorizontalSurface = require( 'ENERGY_FORMS_AND_CHANGES/common/model/HorizontalSurface' );
  var inherit = require( 'PHET_CORE/inherit' );
  var ModelElement = require( 'ENERGY_FORMS_AND_CHANGES/common/model/ModelElement' );
  var ObservableArray = require( 'AXON/ObservableArray' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Rectangle = require( 'DOT/Rectangle' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var SIDE_LENGTH = 0.075; // in meters
  var MAX_ENERGY_GENERATION_RATE = 5000; // joules/sec, empirically chosen
  var CONTACT_DISTANCE = 0.001; // in meters
  var ENERGY_CHUNK_CAPTURE_DISTANCE = 0.2; // in meters, empirically chosen

  // Because of the way that energy chunks are exchanged between thermal modeling elements within this simulation,
  // things can end up looking a bit odd if a burner is turned on with nothing on it.  To account for this, a separate
  // energy generation rate is used when a burner is exchanging energy directly with the air.
  // Units: joules/sec, multiplier empirically chosen.
  var MAX_ENERGY_GENERATION_RATE_INTO_AIR = MAX_ENERGY_GENERATION_RATE * 0.3;

  // counter used by constructor to create unique IDs for each burner
  var idCounter = 0;

  /**
   * @param {Vector2} position - the position in model space where this burner exists
   * @param {Property.<boolean>} energyChunksVisibleProperty - controls whether the energy chunks are visible
   * @constructor
   */
  function Burner( position, energyChunksVisibleProperty ) {

    var self = this;
    ModelElement.call( this, position );

    // @public (read-only) {string} - unique ID, used for debug
    this.id = 'burner-' + idCounter++;

    // @public {Property<number>}
    this.heatCoolLevelProperty = new Property( 0 );

    // @public (read-only) {ObservableArray.<EnergyChunk>}
    this.energyChunkList = new ObservableArray();

    // @private {Vector2}
    this.position = position;

    // @private {Object[]} - motion strategies that control the movement of the energy chunks owned by this burner
    this.energyChunkMotionStrategies = [];

    // @private {Range} - used to keep incoming energy chunks from wandering very far to the left or right
    this.incomingEnergyChunkWanderBounds = new Range( position.x - SIDE_LENGTH / 4, position.x + SIDE_LENGTH / 4 );

    // @private {Property.<boolean>}
    this.energyChunksVisibleProperty = energyChunksVisibleProperty;

    // add position test bounds (see definition in base class for more info)
    this.relativePositionTestingBoundsList.push( new Bounds2( -SIDE_LENGTH / 2, 0, SIDE_LENGTH / 2, SIDE_LENGTH ) );

    // Create and add the top surface.  Some compensation for perspective is necessary in order to avoid problems with
    // edge overlap when dropping objects on top of burner.
    var compositeBounds = self.getCompositeBounds();
    var perspectiveCompensation = compositeBounds.height * EFACConstants.BURNER_EDGE_TO_HEIGHT_RATIO *
                                  Math.cos( EFACConstants.BURNER_PERSPECTIVE_ANGLE ) / 2;

    // @public - see base class for description
    this.topSurface = new HorizontalSurface(
      new Vector2( this.position.x, compositeBounds.maxY ),
      compositeBounds.maxX + perspectiveCompensation - ( compositeBounds.minX - perspectiveCompensation ),
      this
    );
  }

  energyFormsAndChanges.register( 'Burner', Burner );

  return inherit( ModelElement, Burner, {

    /**
     * Get a rectangle that defines the outline of the burner.  In the model the burner is essentially a 2D rectangle.
     * @returns {Rectangle} - rectangle that defines the outline in model space.
     * @public
     */
    getCompositeBounds: function() {
      // TODO: This is wasteful to reconstruct this every time, since burners don't move, should be optimized.
      var minX = this.position.x - SIDE_LENGTH / 2;
      var minY = this.position.y;
      return new Bounds2( minX, minY, minX + SIDE_LENGTH, minY + SIDE_LENGTH
      );
    },

    /**
     * @returns {HorizontalSurface}
     * @public
     */
    getTopSurface: function() {
      return this.topSurface;
    },

    /**
     * Interact with a thermal energy container, adding or removing energy based on the current heat/cool setting.
     * NOTE: this shouldn't be used for air - there is a specific method for that
     * @param {ThermalEnergyContainer} thermalEnergyContainer - model object that will get or give energy
     * @param {number} dt - amount of time (delta time)
     * @returns {number} - amount of energy transferred in joules, can be negative if energy was drawn from object
     * @public
     */
    addOrRemoveEnergyToFromObject: function( thermalEnergyContainer, dt ) {

      var deltaEnergy = 0;

      if ( this.inContactWith( thermalEnergyContainer ) ) {
        if ( thermalEnergyContainer.getTemperature() > EFACConstants.WATER_FREEZING_POINT_TEMPERATURE ) {
          deltaEnergy = MAX_ENERGY_GENERATION_RATE * this.heatCoolLevelProperty.value * dt;

          // make sure we don't cool the object below its minimum allowed energy level
          if ( this.heatCoolLevelProperty.value < 0 ) {
            if ( Math.abs( deltaEnergy ) > thermalEnergyContainer.getEnergyAboveMinimum() ) {
              deltaEnergy = -thermalEnergyContainer.getEnergyAboveMinimum();
            }
          }
        }
        thermalEnergyContainer.changeEnergy( deltaEnergy );
      }
      return deltaEnergy;
    },

    /**
     * Exchange energy with the air.  This has specific behavior that is different from addOrRemoveEnergyToFromObject,
     * defined elsewhere in this type.
     * @param {Air} air - air as a thermal energy container
     * @param dt - amount of time (delta time)
     * @returns {number} - energy, in joules, transferred to the air, negative if energy was absorbed
     * @public
     */
    addOrRemoveEnergyToFromAir: function( air, dt ) {
      var deltaEnergy = MAX_ENERGY_GENERATION_RATE_INTO_AIR * this.heatCoolLevelProperty.value * dt;
      air.changeEnergy( deltaEnergy );
      return deltaEnergy;
    },

    /**
     * determine if the burner is in contact with a thermal energy container
     * @param {ThermalEnergyContainer} thermalEnergyContainer
     * @returns {boolean}
     * @public
     */
    inContactWith: function( thermalEnergyContainer ) {
      var burnerRect = this.getCompositeBounds();
      var area = thermalEnergyContainer.thermalContactArea;
      var xContact = ( area.centerX > burnerRect.minX && area.centerX < burnerRect.maxX );
      var yContact = ( Math.abs( area.minY - burnerRect.maxY ) < CONTACT_DISTANCE );
      return ( xContact && yContact );
    },

    /**
     * @param {EnergyChunk} energyChunk
     * @public
     */
    addEnergyChunk: function( energyChunk ) {

      // make sure the chunk is at the front (which makes it fully opaque in the view)
      energyChunk.zPositionProperty.value = 0;

      // create a motion strategy that will move this energy chunk
      var motionStrategy = new EnergyChunkWanderController(
        energyChunk,
        new Property( this.getCenterPoint() ),
        { horizontalWanderConstraint: this.incomingEnergyChunkWanderBounds, wanderAngleVariation: Math.PI * 0.15 }
      );

      energyChunk.zPosition = 0;

      // add the chunk and its motion strategy to this model
      this.energyChunkList.add( energyChunk );
      this.energyChunkMotionStrategies.push( motionStrategy );
    },

    /**
     * request an energy chunk from the burner
     * @param {Vector2} point - point from which to search for closest chunk
     * @returns {EnergyChunk} - closest energy chunk, null if none are contained
     * @public
     */
    extractEnergyChunkClosestToPoint: function( point ) {
      var self = this;
      var closestEnergyChunk = null;
      if ( this.energyChunkList.length > 0 ) {
        this.energyChunkList.forEach( function( energyChunk ) {
          if ( energyChunk.positionProperty.value.distance( self.position ) > ENERGY_CHUNK_CAPTURE_DISTANCE &&
               ( closestEnergyChunk === null ||
                 energyChunk.positionProperty.value.distance( point ) <
                 closestEnergyChunk.positionProperty.value.distance( point ) ) ) {

            // found a closer chunk
            closestEnergyChunk = energyChunk;
          }
        } );

        this.energyChunkList.remove( closestEnergyChunk );
        this.energyChunkMotionStrategies.forEach( function( energyChunkWanderController, index ) {
          if ( energyChunkWanderController.energyChunk === closestEnergyChunk ) {
            self.energyChunkMotionStrategies.splice( index, 1 );
          }
        } );
      }

      if ( closestEnergyChunk === null && this.heatCoolLevelProperty.value > 0 ) {

        // create an energy chunk
        closestEnergyChunk = new EnergyChunk(
          EnergyType.THERMAL,
          this.getCenterPoint(), // will originate from the center of this burner
          new Vector2( 0, 0 ),
          this.energyChunksVisibleProperty
        );
      }

      if ( closestEnergyChunk === null ) {

        // This probably shouldn't ever occur, but it doesn't really warrant an assert, so log a warning that will
        // encourage investigation if it DOES happen.
        console.warn( 'Burner was unable to supply energy chunks.' );
      }
      return closestEnergyChunk;
    },

    /**
     * request an energy chunk from the burner based on provided bounds
     * @param {Bounds2} bounds - bounds to which the energy chunks will be heading
     * @returns {EnergyChunk} - closest energy chunk
     * @public
     */
    extractEnergyChunkClosestToBounds: function( bounds ) {

      // verify that the bounds where the energy chunk is going are above this burner
      var burnerBounds = this.getCompositeBounds();
      assert && assert(
      bounds.minY >= burnerBounds.maxY && bounds.centerX > burnerBounds.minX && bounds.centerX < burnerBounds.maxX,
        'items should only be on top of burner when getting ECs'
      );
      return this.extractEnergyChunkClosestToPoint( new Vector2( bounds.centerX, bounds.minY ) );
    },

    /**
     * @returns {Vector2}
     * @public
     */
    getCenterPoint: function() {
      return new Vector2( this.position.x, this.position.y + SIDE_LENGTH / 2 );
    },

    /**
     * @public
     */
    reset: function() {
      ModelElement.prototype.reset.call( this );
      this.energyChunkList.clear();
      this.heatCoolLevelProperty.reset();
    },

    /**
     * @param {ThermalEnergyContainer[]} thermalEnergyContainers
     * @returns {boolean}
     * @public
     */
    areAnyOnTop: function( thermalEnergyContainers ) {
      var self = this; // Provide a handle for use in nested callback
      var onTop = false;
      thermalEnergyContainers.forEach( function( thermalEnergyContainer ) {
        if ( self.inContactWith( thermalEnergyContainer ) ) {
          onTop = true;
        }
      } );
      return onTop;
    },

    /**
     * animate the energy chunks
     * @param {number} dt
     * @private
     */
    step: function( dt ) {
      var self = this;
      var controllers = this.energyChunkMotionStrategies.slice();

      controllers.forEach( function( controller ) {

        controller.updatePosition( dt );

        if ( controller.isDestinationReached() ) {
          self.energyChunkList.remove( controller.energyChunk );
          _.pull( self.energyChunkMotionStrategies, controller );
        }
      } );
    },

    /**
     * get a rectangle in model space the corresponds to where the flame and ice exist
     * @returns {Rectangle}
     * @public
     */
    getFlameIceRect: function() {

      // word of warning: this needs to stay consistent with the view
      var outlineRect = this.getCompositeBounds();
      var width = outlineRect.width;
      var height = outlineRect.height;
      return new Rectangle( outlineRect.centerX - width / 4, outlineRect.centerY, width / 2, height / 2 );
    },

    /**
     * get burner temperature, clamped at minimum to the freezing point of water
     * @returns {number}
     * @public
     */
    getTemperature: function() {
      var temperature = EFACConstants.ROOM_TEMPERATURE + this.heatCoolLevelProperty.value * 100;
      return Math.max( temperature, EFACConstants.WATER_FREEZING_POINT_TEMPERATURE );
    }

  } );
} );

<|endoftext|>


<commit_before>import ms from 'ms';

const isEmpty = value => value === undefined || value === null;

export default getCacheConfig => resolver => async (_, args, ctx, info) => {
  let config =
    typeof getCacheConfig === 'function'
      ? await getCacheConfig(_, args, ctx, info)
      : getCacheConfig;

  if (typeof config === 'string') {
    config = {
      id: `${config}:${JSON.stringify(args)}`,
    };
  }

  const { id, policy: policyName, ttl } = { ttl: 0, policy: 'graphqlResolvers', ...config };

  if (!id) {
    return resolver(_, args, ctx, info);
  }

  const policy = ctx.cache.getPolicy(policyName);

  let result = await policy.get(id);

  if (isEmpty(result)) {
    result = await resolver(_, args, ctx, info);

    if (!isEmpty(result)) {
      await policy.set(id, result, typeof ttl === 'string' ? ms(ttl) : ttl);
    }
  }

  return result;
};
<commit_msg>[oors-graphql] withCache decorator will ignore empty args<commit_after>import ms from 'ms';

const isEmpty = value => value === undefined || value === null;

export default getCacheConfig => resolver => async (_, args, ctx, info) => {
  let config =
    typeof getCacheConfig === 'function'
      ? await getCacheConfig(_, args, ctx, info)
      : getCacheConfig;

  if (typeof config === 'string') {
    const serializedArgs = Object.keys(args).length
      ? JSON.stringify(args, Object.keys(args).sort())
      : '';
    config = {
      id: `${config}${serializedArgs ? `:${serializedArgs}` : ''}`,
    };
  }

  const { id, policy: policyName, ttl } = { ttl: 0, policy: 'graphqlResolvers', ...config };

  if (!id) {
    return resolver(_, args, ctx, info);
  }

  const policy = ctx.cache.getPolicy(policyName);

  let result = await policy.get(id);

  if (isEmpty(result)) {
    result = await resolver(_, args, ctx, info);

    if (!isEmpty(result)) {
      await policy.set(id, result, typeof ttl === 'string' ? ms(ttl) : ttl);
    }
  }

  return result;
};
<|endoftext|>


<commit_before>import $path from 'path'

export class Router {
	_scopedAction = null
	_scopedPrefix = ''

	app = null
	bindings = { }
	patterns = { }

	constructor(app) {
		this.app = app
	}

	group(action, callback) {
		this._scopedAction = Object.assign({ }, action)

		if(this._scopedAction.prefix) {
			this._scopedPrefix = this._normalizePathComponent(this._scopedAction.prefix)
			delete this._scopedAction.prefix
		} else {
			this._scopedPrefix = ''
		}

		callback(this, action.controller)

		this._scopedAction = null
		this._scopedPrefix = ''
	}

	all(path, action) {
		console.log('Don’t use `all` routes. Offender: %s', path)
		return this.app.all(this._scopedPrefix + path, this._makeAction(action))
	}

	get(path, action, context) {
		return this._add('get', path, action, context)
	}

	post(path, action, context) {
		return this._add('post', path, action, context)
	}

	put(path, action, context) {
		return this._add('put', path, action, context)
	}

	patch(path, action, context) {
		return this._add('patch', path, action, context)
	}

	delete(path, action, context) {
		return this._add('delete', path, action, context)
	}

	_add(method, path, action, context) {
		const handler = this._makeAction(action)

		// WARNING: Prone to failure if ExpressJS changes this logic
		this.app.lazyrouter()

		const handlers = [ handler ]

		if(typeof action.use !== 'undefined') {
			if(Array.isArray(action.use)) {
				action.use = {
					before: action.use
				}
			}

			if(typeof action.use === 'object') {
				if(Array.isArray(action.use.before)) {
					handlers.unshift(...action.use.before)
				} else if(Array.isArray(action.use.before)) {
					handlers.push(...action.use.after)
				}
			}
		}

		path = this._normalizePathComponent(path)
		const compiledPath = $path.join('/', this._scopedPrefix, path).replace(/:([a-z0-0_\-\.]+)/g, (param, name) => {
			const pattern = this.patterns[name]

			if(pattern.isNil) {
				return param
			} else {
				return param + '(' + pattern + ')'
			}
		})

		let route = this.app._router.route(compiledPath)
		route = route[method](...handlers)
		route.context = context || {}

		return route
	}

	_makeAction(action) {
		if(typeof action === 'function') {
			return action
		}

		if(typeof action === 'string') {
			action = { method: action }
		}

		action = Object.assign({ }, this._scopedAction, action)
		const method = action.controller[action.method]
		const controller = action.controller

		return (...args) => {
			const result = method.apply(controller, args)

			if(typeof result === 'object' && typeof result.catch === 'function') {
				return result.catch(args[2])
			} else {
				return result
			}
		}
	}

	_normalizePathComponent(component) {
		return $path.normalize(component.trim()).replace(/^\//, '')
	}

	bind(name, resolver, context) {
		this.bindings[name] = { resolver }

		if(!context.isNil) {
			this.bindings[name].context = context
		}

		this.app.param(name, (req, res, next, value) => {
			resolver(value, newValue => {
				req.params[name] = newValue
				next()
			}, err => {
				next(err)
			}, req, res)
		})
	}

	pattern(name, pattern) {
		this.patterns[name] = pattern
	}

}
<commit_msg>Route groups are now nestable<commit_after>import $path from 'path'

export class Router {
	app = null
	bindings = { }
	patterns = { }

	_scopedActionStack = [ { } ]
	_scopedPrefixStack = [ '/' ]

	get _scopedAction() {
		return this._scopedActionStack[this._scopedActionStack.length - 1]
	}

	get _scopedPrefix() {
		return this._scopedPrefixStack[this._scopedPrefixStack.length - 1]
	}

	constructor(app) {
		this.app = app
	}

	group(action, callback) {
		const parentAction = this._scopedAction
		const parentPrefix = this._scopedPrefix

		const scopedAction = Object.assign({ }, parentAction, action)
		let scopedPrefix = scopedAction.prefix
		delete scopedAction.prefix
		this._scopedActionStack.push(scopedAction)

		if(!scopedPrefix.isNil && scopedPrefix.length > 0) {
			if(!parentPrefix.isNil && parentPrefix.length > 0) {
				scopedPrefix = $path.join(parentPrefix, scopedPrefix)
			}

			this._scopedPrefixStack.push(this._normalizePathComponent(scopedPrefix))
		} else {
			this._scopedPrefixStack.push(parentPrefix)
		}

		callback(this, action.controller)

		this._scopedActionStack.pop()
		this._scopedPrefixStack.pop()
	}

	all(path, action) {
		console.log('Don’t use `all` routes. Offender: %s', path)
		return this.app.all(this._scopedPrefix + path, this._makeAction(action))
	}

	get(path, action, context) {
		return this._add('get', path, action, context)
	}

	post(path, action, context) {
		return this._add('post', path, action, context)
	}

	put(path, action, context) {
		return this._add('put', path, action, context)
	}

	patch(path, action, context) {
		return this._add('patch', path, action, context)
	}

	delete(path, action, context) {
		return this._add('delete', path, action, context)
	}

	_add(method, path, action, context) {
		const handler = this._makeAction(action)

		// WARNING: Prone to failure if ExpressJS changes this logic
		this.app.lazyrouter()

		const handlers = [ handler ]

		if(typeof action.use !== 'undefined') {
			if(Array.isArray(action.use)) {
				action.use = {
					before: action.use
				}
			}

			if(typeof action.use === 'object') {
				if(Array.isArray(action.use.before)) {
					handlers.unshift(...action.use.before)
				} else if(Array.isArray(action.use.before)) {
					handlers.push(...action.use.after)
				}
			}
		}

		path = this._normalizePathComponent(path)
		const compiledPath = $path.join('/', this._scopedPrefix, path).replace(/:([a-z0-0_\-\.]+)/g, (param, name) => {
			const pattern = this.patterns[name]

			if(pattern.isNil) {
				return param
			} else {
				return param + '(' + pattern + ')'
			}
		})

		let route = this.app._router.route(compiledPath)
		route = route[method](...handlers)
		route.context = context || {}

		return route
	}

	_makeAction(action) {
		if(typeof action === 'function') {
			return action
		}

		if(typeof action === 'string') {
			action = { method: action }
		}

		action = Object.assign({ }, this._scopedAction, action)
		const method = action.controller[action.method]
		const controller = action.controller

		return (...args) => {
			const result = method.apply(controller, args)

			if(typeof result === 'object' && typeof result.catch === 'function') {
				return result.catch(args[2])
			} else {
				return result
			}
		}
	}

	_normalizePathComponent(component) {
		return $path.normalize(component.trim()).replace(/^\//, '')
	}

	bind(name, resolver, context) {
		this.bindings[name] = { resolver }

		if(!context.isNil) {
			this.bindings[name].context = context
		}

		this.app.param(name, (req, res, next, value) => {
			resolver(value, newValue => {
				req.params[name] = newValue
				next()
			}, err => {
				next(err)
			}, req, res)
		})
	}

	pattern(name, pattern) {
		this.patterns[name] = pattern
	}

}
<|endoftext|>


<commit_before>"use strict";

var path = require("path");

function _logDoclet (doclet) {
    var title = [];
    if (doclet.meta && doclet.meta.lineno) {
        title.push("@", doclet.meta.lineno, ": ");
    }
    if (doclet.undocumented) {
        title.push("(u) ");
    }
    title.push(doclet.longname, " (", doclet.kind, ")");
    console.log(title.join(""));
    // try {
    //     if (doclet.longname === "_GpfDate") {
    //         console.log(doclet);
    //     }
    // } catch (e) {
    //     // ignore
    // }
}

function _findDoclet (doclets, longname, tag) {
    var resultDoclet;
    doclets.every(function (doclet) {
        if (doclet.longname === longname && !doclet.undocumented) {
            resultDoclet = doclet;
            return false;
        }
        return true;
    });
    if (!resultDoclet) {
        throw new Error("invalid reference for @" + tag.title);
    }
    return resultDoclet;
}

function _findRefDoclet (doclets, doclet, tag) {
    var refLongname = tag.value;
    if (-1 === refLongname.indexOf("#") && -1 !== doclet.longname.indexOf("#")) {
        refLongname = doclet.longname.split("#")[0] + "#" + refLongname;
    }
    return _findDoclet(doclets, refLongname, tag);
}

var _customTags = {

    // Returns the same type with a generic comment
    chainable: function (doclet/*, tag, doclets*/) {
        doclet.returns = [{
            type: {
                names: [doclet.memberof]
            },
            description: "Self reference to allow chaining"
        }];
    },

    // Read accessor on a property
    read: function (doclet, tag, doclets) {
        var refDoclet = _findRefDoclet(doclets, doclet, tag);
        doclet.returns = [{
            type: refDoclet.type,
            description: refDoclet.description
        }];
        doclet.description = "GET accessor for " + refDoclet.description;
    },

    // Write accessor on a property
    write: function (/*doclet, tag, doclets*/) {
        // var refDoclet = _findRefDoclet(doclets, doclet, tag);
    },

    // Same as another doclet
    sameas: function (doclet, tag, doclets) {
        var refDoclet = _findRefDoclet(doclets, doclet, tag);
        [
            "description",
            "params",
            "returns",
            "kind"
        ].forEach(function (propertyName) {
            doclet[propertyName] = refDoclet[propertyName];
        });
    }

};

function _handleCustomTags (doclet, doclets) {
    if (doclet.tags) {
        doclet.tags.forEach(function (tag) {
            var handler = _customTags[tag.title];
            if (undefined !== handler) {
                try {
                    handler(doclet, tag, doclets);
                } catch (e) {
                    console.error(doclet.meta.path + path.sep + doclet.meta.filename + "@" + doclet.meta.lineno + ": "
                        + e.message);
                    console.error(doclet);
                    throw e;
                }
            }
        });
    }
}

function _addMemberType (doclet) {
    if (!doclet.type) {
        // type: { names: [ 'String' ] }
        var codeType = doclet.meta.code.type,
            type;
        if (codeType === "Literal") {
            type = typeof doclet.meta.code.value;
            type = type.charAt(0).toUpperCase() + type.substr(1);
        } else if (codeType === "ArrayExpression") {
            type = "Array";
        } else if (codeType === "ObjectExpression") {
            type = "Object";
        }
        doclet.type = {
            names: [
                type
            ]
        };
    }
}

function _checkAccess (doclet) {
    if (!doclet.access) {
        if (doclet.name.charAt(0) === "_") {
            doclet.access = "private";
        } else {
            doclet.access = "public";
        }
    }
}

function _postProcessDoclet (doclet, index, doclets) {
    var kind = doclet.kind;
    _logDoclet(doclet);
    _handleCustomTags(doclet, doclets);
    if (kind === "member") {
        _addMemberType(doclet);
        _checkAccess(doclet);
    } else if (-1 !== ["function", "class"].indexOf(kind)) {
        _checkAccess(doclet);
    }
}

// http://usejsdoc.org/about-plugins.html
module.exports = {

    handlers: {

        processingComplete: function (event) {
            event.doclets.forEach(_postProcessDoclet);
        }

    }

};
<commit_msg>Prefix jsdoc extensions with gpf<commit_after>"use strict";

var path = require("path");

function _logDoclet (doclet) {
    var title = [];
    if (doclet.meta && doclet.meta.lineno) {
        title.push("@", doclet.meta.lineno, ": ");
    }
    if (doclet.undocumented) {
        title.push("(u) ");
    }
    title.push(doclet.longname, " (", doclet.kind, ")");
    console.log(title.join(""));
    // try {
    //     if (doclet.longname === "<anonymous>~XhrRequest#headers") {
    //         console.log(doclet);
    //     }
    // } catch (e) {
    //     // ignore
    // }
}

function _findDoclet (doclets, longname, tag) {
    var resultDoclet;
    doclets.every(function (doclet) {
        if (doclet.longname === longname && !doclet.undocumented) {
            resultDoclet = doclet;
            return false;
        }
        return true;
    });
    if (!resultDoclet) {
        throw new Error("invalid reference for @" + tag.title);
    }
    return resultDoclet;
}

function _findRefDoclet (doclets, doclet, tag) {
    var refLongname = tag.value;
    if (-1 === refLongname.indexOf("#") && -1 !== doclet.longname.indexOf("#")) {
        refLongname = doclet.longname.split("#")[0] + "#" + refLongname;
    }
    return _findDoclet(doclets, refLongname, tag);
}

var _customTags = {

    // Returns the same type with a generic comment
    chainable: function (doclet/*, tag, doclets*/) {
        doclet.returns = [{
            type: {
                names: [doclet.memberof]
            },
            description: "Self reference to allow chaining"
        }];
    },

    // Read accessor on a property
    read: function (doclet, tag, doclets) {
        var refDoclet = _findRefDoclet(doclets, doclet, tag);
        doclet.returns = [{
            type: refDoclet.type,
            description: refDoclet.description
        }];
        doclet.description = "GET accessor for " + refDoclet.description;
    },

    // Write accessor on a property
    write: function (/*doclet, tag, doclets*/) {
        // var refDoclet = _findRefDoclet(doclets, doclet, tag);
    },

    // Same as another doclet
    sameas: function (doclet, tag, doclets) {
        var refDoclet = _findRefDoclet(doclets, doclet, tag);
        [
            "description",
            "params",
            "returns",
            "kind"
        ].forEach(function (propertyName) {
            doclet[propertyName] = refDoclet[propertyName];
        });
    }

};

function _handleCustomTags (doclet, doclets) {
    if (doclet.tags) {
        doclet.tags.forEach(function (tag) {
            var customTag = tag.title.split("gpf:")[1];
            var handler = _customTags[customTag];
            if (undefined !== handler) {
                try {
                    handler(doclet, tag, doclets);
                } catch (e) {
                    console.error(doclet.meta.path + path.sep + doclet.meta.filename + "@" + doclet.meta.lineno + ": "
                        + e.message);
                    console.error(doclet);
                    throw e;
                }
            }
        });
    }
}

function _addMemberType (doclet) {
    if (!doclet.type) {
        // type: { names: [ 'String' ] }
        var codeType = doclet.meta.code.type,
            type;
        if (codeType === "Literal") {
            type = typeof doclet.meta.code.value;
            type = type.charAt(0).toUpperCase() + type.substr(1);
        } else if (codeType === "ArrayExpression") {
            type = "Array";
        } else if (codeType === "ObjectExpression") {
            type = "Object";
        }
        doclet.type = {
            names: [
                type
            ]
        };
    }
}

function _checkAccess (doclet) {
    if (!doclet.access) {
        if (doclet.name.charAt(0) === "_") {
            doclet.access = "private";
        } else {
            doclet.access = "public";
        }
    }
}

function _postProcessDoclet (doclet, index, doclets) {
    var kind = doclet.kind;
    _logDoclet(doclet);
    _handleCustomTags(doclet, doclets);
    if (kind === "member") {
        _addMemberType(doclet);
        _checkAccess(doclet);
    } else if (-1 !== ["function", "class"].indexOf(kind)) {
        _checkAccess(doclet);
    }
}

// http://usejsdoc.org/about-plugins.html
module.exports = {

    handlers: {

        processingComplete: function (event) {
            event.doclets.forEach(_postProcessDoclet);
        }

    }

};
<|endoftext|>


<commit_before>window.requestAnimFrame = function(){
    return (
            window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function(/* function */ callback){
                window.setTimeout(callback, 1000 / 60);
            }
        );
}();

(function( window, document) {
  var Whiteboard = function( host, element ) {
    this.whiteboard = element;
    this.messageBus = new MessageBus()
                            .subscribe( 'receive_draw',   this.handleUpdate.bind(this) )
                            .subscribe( 'receive_pen_up', this.handlePenUp.bind(this) );
                            // .subscribe( 'set_penColor',   this.handleSetPenColor.bind(this) )
                            // .subscribe( 'set_penWidth',   this.handleSetPenWidth.bind(this) );

    this.client = new WhiteboardClient( host, this.messageBus );

    // sizing ( 2x larger than 1080 )
    this.width = 1920 * 2;
    this.height = 1080 * 2;

    // initialize offscreen context
    this.image = this.createImage( this.width, this.height );
    this.imageCtx = this.image.getContext('2d');
    this.whiteboardCtx = this.whiteboard.getContext('2d');

    // for pinch-to-zoom
    // this stores the size of the last box, formed by taking the 2 finger
    // touchpoints and creating a rectangle and calculating its area
    // lastZoomCenter stores the x/y (as an array) of the center of the zoom box.
    // this allows the ability to scroll around, too
    this.lastZoomBoxSize = null; // an integer
    this.lastZoomCenter = null;  // to be populated with array of [ x, y ]

    // properties of the local client
    this.zoomRatio = 1;

    // the x/y of the view's upper-left corner compared to the main image
    // this is actual X/Y, in pixels of the source image at actual size.
    // by default , we want to be centered.
    this.scrollX = ( this.width / 2 ) - ( this.translateZoomFromLocalToFullsize(this.whiteboard.width) / 2 );
    this.scrollY = ( this.height / 2 ) - ( this.translateZoomFromLocalToFullsize(this.whiteboard.height) / 2 );

    // initialize some of our state
    this.penWidth = 4;
    this.penColor = "0000FF";

    // local state of the pen
    // (used for tracking the difference between mousedrag and mousemove)
    this.penDown = false;

    // store where each user's pen is (for connecting lines)
    this.penStatuses = {}; // hash keyed by userId

    // some initialization functions
    this.resizeCanvasToWindow();
    this.initializeListeners();
  };

  // initialize the offscreen image whiteboard and return it
  Whiteboard.prototype.createImage = function( width, height ) {
    var buffer = document.createElement('canvas'),
        ctx = buffer.getContext('2d');

    buffer.width = width;
    buffer.height = height;

    // fill with white
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, width, height);

    return buffer;
  }

  // resizes the onscreen canvas in the DOM to the size of the window
  Whiteboard.prototype.resizeCanvasToWindow = function() {
    // resize the canvas a bit
    var body = document.getElementsByTagName('body')[0],
        fullsizeWidth = body.offsetWidth,
        fullsizeHeight = window.innerHeight;

    this.whiteboard.width = fullsizeWidth;
    this.whiteboard.height = fullsizeHeight;
  };

  // convenience method for adding event listener to the whiteboard canvas element
  Whiteboard.prototype.handle = function( eventName, callback ) {
    this.whiteboard.addEventListener( eventName, callback, false );
  };

  // hook into all the event handlers
  Whiteboard.prototype.initializeListeners = function() {
    // touch
    this.handle( 'touchstart', this.handleTouchStart.bind(this) );
    this.handle( 'touchend',   this.handleTouchEnd.bind(this)   );
    this.handle( 'touchmove',  this.handleTouchMove.bind(this)  );

    // mouse
    this.handle( 'mousedown',  this.handleMouseDown.bind(this)  );
    this.handle( 'mouseup',    this.handleMouseUp.bind(this)    );
    this.handle( 'mousemove',  this.handleMouseMove.bind(this)  );

    this.handle( 'mousewheel', function(event) {
      event.preventDefault();

      var dx = event.wheelDeltaX,
          dy = event.wheelDeltaY,
          inverted = event.webkitDirectionInvertedFromDevice,
          scrollDx = this.translateZoomFromLocalToFullsize( dx ),
          scrollDy = this.translateZoomFromLocalToFullsize( dy );

      this.scroll( scrollDx, scrollDy, inverted );
    }.bind(this))
  };

  Whiteboard.prototype.translateZoomFromLocalToFullsize = function( d ) {
    return d / this.zoomRatio;
  };

  Whiteboard.prototype.translateZoomFromFullsizeToLocal = function( d ) {
    return d * this.zoomRatio;
  };

  // return an object with x and y keys
  // translates from canvas x/y to fullsize image x/y
  // takes scrolling and zooming into account
  Whiteboard.prototype.translateFromLocalToFullsize = function( x, y ) {
    return {
      x: this.scrollX + ( x / this.zoomRatio ),
      y: this.scrollY + ( y / this.zoomRatio )
    };
  };

  Whiteboard.prototype.translateFromFullsizeToLocal = function( x, y ) {
    return {
      x: ( x * this.zoomRatio ) - this.scrollX,
      y: ( y * this.zoomRatio ) - this.scrollY
    };
  };

  // given a width and height of the viewport, get width and height in fullsize
  Whiteboard.prototype.translateDimensionsFromLocalToFullsize = function( w, h ) {
    return {
      width: w / this.zoomRatio,
      height: h / this.zoomRatio
    };
  };

  Whiteboard.prototype.translateDimensionsFromFullsizeToLocal = function( w, h ) {
    return {
      width: w * this.zoomRatio,
      height: h * this.zoomRatio
    };
  };

  /*
   *  scroll deltas
   *  this uses actual image size values
   */
  Whiteboard.prototype.scroll = function( dx, dy, is_inverted, doRedraw ) {
    if ( typeof is_inverted === 'undefined' ) { is_inverted = false; }
    if ( typeof doRedraw === 'undefined' ) { doRedraw = true; }

    if (is_inverted) {
      dx = -dx;
      dy = -dy;
    }

    this.scrollX -= dx;
    this.scrollY -= dy;

    this.scrollTo( undefined, undefined, doRedraw );
  };

  Whiteboard.prototype.scrollTo = function( x, y, doRedraw ) {
    if ( typeof x === 'undefined' ) { x = this.scrollX; }
    if ( typeof y === 'undefined' ) { y = this.scrollY; }
    if ( typeof doRedraw === 'undefined' ) { doRedraw = true; }

    var whiteboardDimensions = this.translateDimensionsFromLocalToFullsize( this.whiteboard.width, this.whiteboard.height ),
        maxXScroll = this.image.width - whiteboardDimensions.width,
        maxYScroll = this.image.height - whiteboardDimensions.height;

    this.scrollX = x;
    this.scrollY = y;

    if (this.scrollX < 0) {
      this.scrollX = 0;
    } else if ( this.scrollX > maxXScroll ) {
      this.scrollX = maxXScroll;
    }

    if (this.scrollY < 0) {
      this.scrollY = 0;
    } else if ( this.scrollY > maxYScroll ) {
      this.scrollY = maxYScroll;
    }

    if ( doRedraw ) {
      this.redraw();
    }
  };

  Whiteboard.prototype.redraw = function() {
    if ( this.currentlyDrawing ) { return; }

    this.currentlyDrawing = true;
    window.requestAnimFrame(function() {

      // the width/height of the visible section of the source image
      // based on the whiteboard's dimensions (with zoom taken into account)
      var whiteboardDimensions = this.translateDimensionsFromLocalToFullsize( this.whiteboard.width, this.whiteboard.height ),
          sourceWidth = whiteboardDimensions.width,
          sourceHeight = whiteboardDimensions.height;

      this.whiteboardCtx.drawImage(
        this.image,

        // where on the source image to draw from
        this.scrollX,
        this.scrollY,
        sourceWidth,
        sourceHeight,

        // where on the local canvas to draw to
        0,
        0,
        this.whiteboard.width,
        this.whiteboard.height
      );

      this.currentlyDrawing = false;
    }.bind(this));
  };

  Whiteboard.prototype.setZoom = function( newZoom ) {
    this.zoomRatio = newZoom;

    // restrict to minimum and maximum zoom levels.
    if ( isNaN(this.zoomRatio) ) { this.zoomRatio = 1; }
    if ( this.zoomRatio < .1 ) { this.zoomRatio = .1; }
    if ( this.zoomRatio > 2 ) { this.zoomRatio = 2; }

    return this.zoomRatio;
  };

  // functions for sending things to the server
  // TODO: this should probably be part of the client itself.

  Whiteboard.prototype.sendDrawEvent = function( pointer, x, y ) {
    var translatedCoordinates = this.translateFromLocalToFullsize( x, y );

    this.messageBus.broadcast( 'draw', {
      pointer: pointer,
      x: translatedCoordinates.x,
      y: translatedCoordinates.y,
      penWidth: this.penWidth,
      penColor: this.penColor
    } );
  };

  Whiteboard.prototype.sendPenUp = function() {
    this.client.send( 'pen_up', {} );
  };

  // event receivers
  // for how we handle incoming things

  // receive pen event from server
  Whiteboard.prototype.handleUpdate = function( messageType, message ) {
    var ctx         = this.imageCtx,
        last        = this.penStatuses[message.userId],
        userId      = message.userId.replace(/[^a-z0-9]/ig, ''),
        userEle     = document.getElementById(userId);
        onscreenCtx = this.whiteboardCtx;

    // set background on this user
    userEle.style.backgroundColor = "#" + message.penColor;

    ctx.fillStyle = "#" + message.penColor;
    // ctx.fillRect( message.x, message.y, 2, 2 );

    ctx.beginPath();
    if ( last ) {
      ctx.moveTo(last.x, last.y);
      ctx.lineTo(message.x, message.y);
      ctx.lineWidth = message.penWidth;
      ctx.strokeStyle = "#" + message.penColor;
      ctx.stroke();
    }

    ctx.arc(message.x, message.y, message.penWidth / 2, 0,2*Math.PI, false);
    ctx.fill();

    this.penStatuses[message.userId] = { x: message.x, y: message.y };

    // render offscreen image to onscreen canvas
    this.redraw();
  };

  // receive pen-up from server
  Whiteboard.prototype.handlePenUp = function( messageType, message ) {
    var userId = message.userId
        userEleId = message.userId.replace(/[^a-z0-9]/ig, ''),
        userEle = document.getElementById(userEleId);

    userEle.style.backgroundColor = "transparent";

    delete this.penStatuses[userId];
  };

  Whiteboard.prototype.handlePinchToZoom = function( touches ) {

    // calculate zoomRatio by using the box formed by t1 and t2
    // the ratio is the local size / fullsize size
    var vt1        = { x: touches[0].clientX, y: touches[0].clientY },                  // t1, view scale
        vt2        = { x: touches[1].clientX, y: touches[1].clientY },                  // t2, view scale
        ft1        = this.translateFromLocalToFullsize( vt1.x, vt1.y ),                 // t1 full scale
        ft2        = this.translateFromLocalToFullsize( vt2.x, vt2.y ),                 // t2 full scale
        oft1       = this.zoomTouches[0],                                               // original t1, full scale
        oft2       = this.zoomTouches[1],                                               // original t2, full scale
        vboxWidth  = Math.abs( vt1.x - vt2.x ),                                         // width of the box at screen size
        vboxHeight = Math.abs( vt1.y - vt2.y ),                                         // height of box at screen size
        fboxWidth  = Math.abs( oft1.x - oft2.x ),                                       // width of original box at fullsize
        fboxHeight = Math.abs( oft1.y - oft2.y ),                                       // height of original box at full size
        vboxDiag   = Math.sqrt( Math.pow( vboxWidth, 2 ) + Math.pow( vboxHeight, 2 ) ), // distance between screen-size points
        fboxDiag   = Math.sqrt( Math.pow( fboxWidth, 2 ) + Math.pow( fboxHeight, 2 ) ), // distance between original fullsize points
        zoomRatio  = this.setZoom( vboxDiag / fboxDiag ),                               // ratio of screen size to full size (calculate zoom)

        // scroll values:
        // we have original x/y on fullsize
        // lock touchpoint 1's X from original location to that point on the screen
        dx         = oft1.x - ft1.x,
        dy         = oft1.y - ft1.y;

    this.scroll( dx, dy, true );
  };

  // interaction event handers
  // this is stuff like mouse-move, touch events and keyboard events
  // before they get filtered through our internal API.

  Whiteboard.prototype.handleTouchStart = function( event ) {
    event.preventDefault();

    var touch = event.changedTouches[0];

    if ( event.touches.length == 1 ) {
      this.sendDrawEvent( 'touch', touch.clientX, touch.clientY );
    } else if ( event.touches.length == 2 ) {

      this.zoomTouches = [
        this.translateFromLocalToFullsize( event.touches[0].clientX, event.touches[0].clientY ),
        this.translateFromLocalToFullsize( event.touches[1].clientX, event.touches[1].clientY )
      ];

    }

  };

  Whiteboard.prototype.handleTouchEnd = function( event ) {
    event.preventDefault();

    if ( event.touches.length != 2 ) {
      this.zoomTouches = null;
      this.lastZoomBoxSize = null;
      this.lastZoomCenter = null;
    }

    this.sendPenUp();
  };

  Whiteboard.prototype.handleTouchMove = function( event ) {
    event.preventDefault();

    var touch = event.changedTouches[0];

    if ( event.touches.length == 1 ) {
      this.sendDrawEvent( 'touch', touch.clientX, touch.clientY );
    } else if ( event.touches.length == 2 ) {
      this.handlePinchToZoom( event.touches );
    }
  };

  Whiteboard.prototype.handleMouseDown = function( event ) {
    event.preventDefault();

    this.penDown = true;

    var x = event.clientX,
        y = event.clientY;

    this.sendDrawEvent( 'mouse', x, y );
  };

  Whiteboard.prototype.handleMouseUp = function( event ) {
    event.preventDefault();

    this.penDown = false;

    this.sendPenUp();
  };

  Whiteboard.prototype.handleMouseMove = function( event ) {
    event.preventDefault();

    // if the mouse isn't down, it's not a drag event.
    if ( ! this.penDown ) { return; }

    var x = event.clientX,
        y = event.clientY;

    this.sendDrawEvent( 'mouse', x, y );
  };

  window.Whiteboard = Whiteboard;

})( window, document );
<commit_msg>more cleanup. better handling of touches.<commit_after>window.requestAnimFrame = function(){
    return (
            window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function(/* function */ callback){
                window.setTimeout(callback, 1000 / 60);
            }
        );
}();

(function( window, document) {
  var Whiteboard = function( host, element ) {
    this.whiteboard = element;
    this.messageBus = new MessageBus()
                            .subscribe( 'receive_draw',   this.handleUpdate.bind(this) )
                            .subscribe( 'receive_pen_up', this.handlePenUp.bind(this) );
                            // .subscribe( 'set_penColor',   this.handleSetPenColor.bind(this) )
                            // .subscribe( 'set_penWidth',   this.handleSetPenWidth.bind(this) );

    this.client = new WhiteboardClient( host, this.messageBus );

    // sizing ( 2x larger than 1080 )
    this.width = 1920 * 2;
    this.height = 1080 * 2;

    // initialize offscreen context
    this.image = this.createImage( this.width, this.height );
    this.imageCtx = this.image.getContext('2d');
    this.whiteboardCtx = this.whiteboard.getContext('2d');

    // properties of the local client
    this.zoomRatio = 1;

    // the x/y of the view's upper-left corner compared to the main image
    // this is actual X/Y, in pixels of the source image at actual size.
    // by default , we want to be centered.
    this.scrollX = ( this.width / 2 ) - ( this.translateZoomFromLocalToFullsize(this.whiteboard.width) / 2 );
    this.scrollY = ( this.height / 2 ) - ( this.translateZoomFromLocalToFullsize(this.whiteboard.height) / 2 );

    // initialize some of our state
    this.penWidth = 4;
    this.penColor = "0000FF";

    // local state of the pen
    // (used for tracking the difference between mousedrag and mousemove)
    this.penDown = false;

    // store where each user's pen is (for connecting lines)
    this.penStatuses = {}; // hash keyed by userId

    // some initialization functions
    this.resizeCanvasToWindow();
    this.initializeListeners();
  };

  // initialize the offscreen image whiteboard and return it
  Whiteboard.prototype.createImage = function( width, height ) {
    var buffer = document.createElement('canvas'),
        ctx = buffer.getContext('2d');

    buffer.width = width;
    buffer.height = height;

    // fill with white
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, width, height);

    return buffer;
  }

  // resizes the onscreen canvas in the DOM to the size of the window
  Whiteboard.prototype.resizeCanvasToWindow = function() {
    // resize the canvas a bit
    var body = document.getElementsByTagName('body')[0],
        fullsizeWidth = body.offsetWidth,
        fullsizeHeight = window.innerHeight;

    this.whiteboard.width = fullsizeWidth;
    this.whiteboard.height = fullsizeHeight;
  };

  // convenience method for adding event listener to the whiteboard canvas element
  Whiteboard.prototype.handle = function( eventName, callback ) {
    this.whiteboard.addEventListener( eventName, callback, false );
  };

  // hook into all the event handlers
  Whiteboard.prototype.initializeListeners = function() {
    // touch
    this.handle( 'touchstart', this.handleTouchStart.bind(this) );
    this.handle( 'touchend',   this.handleTouchEnd.bind(this)   );
    this.handle( 'touchmove',  this.handleTouchMove.bind(this)  );

    // mouse
    this.handle( 'mousedown',  this.handleMouseDown.bind(this)  );
    this.handle( 'mouseup',    this.handleMouseUp.bind(this)    );
    this.handle( 'mousemove',  this.handleMouseMove.bind(this)  );

    this.handle( 'mousewheel', function(event) {
      event.preventDefault();

      var dx = event.wheelDeltaX,
          dy = event.wheelDeltaY,
          inverted = event.webkitDirectionInvertedFromDevice,
          scrollDx = this.translateZoomFromLocalToFullsize( dx ),
          scrollDy = this.translateZoomFromLocalToFullsize( dy );

      this.scroll( scrollDx, scrollDy, inverted );
    }.bind(this))
  };

  Whiteboard.prototype.translateZoomFromLocalToFullsize = function( d ) {
    return d / this.zoomRatio;
  };

  Whiteboard.prototype.translateZoomFromFullsizeToLocal = function( d ) {
    return d * this.zoomRatio;
  };

  // return an object with x and y keys
  // translates from canvas x/y to fullsize image x/y
  // takes scrolling and zooming into account
  Whiteboard.prototype.translateFromLocalToFullsize = function( x, y ) {
    return {
      x: this.scrollX + ( x / this.zoomRatio ),
      y: this.scrollY + ( y / this.zoomRatio )
    };
  };

  Whiteboard.prototype.translateFromFullsizeToLocal = function( x, y ) {
    return {
      x: ( x * this.zoomRatio ) - this.scrollX,
      y: ( y * this.zoomRatio ) - this.scrollY
    };
  };

  // given a width and height of the viewport, get width and height in fullsize
  Whiteboard.prototype.translateDimensionsFromLocalToFullsize = function( w, h ) {
    return {
      width: w / this.zoomRatio,
      height: h / this.zoomRatio
    };
  };

  Whiteboard.prototype.translateDimensionsFromFullsizeToLocal = function( w, h ) {
    return {
      width: w * this.zoomRatio,
      height: h * this.zoomRatio
    };
  };

  /*
   *  scroll deltas
   *  this uses actual image size values
   */
  Whiteboard.prototype.scroll = function( dx, dy, is_inverted, doRedraw ) {
    if ( typeof is_inverted === 'undefined' ) { is_inverted = false; }
    if ( typeof doRedraw === 'undefined' ) { doRedraw = true; }

    if (is_inverted) {
      dx = -dx;
      dy = -dy;
    }

    this.scrollX -= dx;
    this.scrollY -= dy;

    this.scrollTo( undefined, undefined, doRedraw );
  };

  Whiteboard.prototype.scrollTo = function( x, y, doRedraw ) {
    if ( typeof x === 'undefined' ) { x = this.scrollX; }
    if ( typeof y === 'undefined' ) { y = this.scrollY; }
    if ( typeof doRedraw === 'undefined' ) { doRedraw = true; }

    var whiteboardDimensions = this.translateDimensionsFromLocalToFullsize( this.whiteboard.width, this.whiteboard.height ),
        maxXScroll = this.image.width - whiteboardDimensions.width,
        maxYScroll = this.image.height - whiteboardDimensions.height;

    this.scrollX = x;
    this.scrollY = y;

    if (this.scrollX < 0) {
      this.scrollX = 0;
    } else if ( this.scrollX > maxXScroll ) {
      this.scrollX = maxXScroll;
    }

    if (this.scrollY < 0) {
      this.scrollY = 0;
    } else if ( this.scrollY > maxYScroll ) {
      this.scrollY = maxYScroll;
    }

    if ( doRedraw ) {
      this.redraw();
    }
  };

  Whiteboard.prototype.redraw = function() {
    if ( this.currentlyDrawing ) { return; }

    this.currentlyDrawing = true;
    window.requestAnimFrame(function() {

      // the width/height of the visible section of the source image
      // based on the whiteboard's dimensions (with zoom taken into account)
      var whiteboardDimensions = this.translateDimensionsFromLocalToFullsize( this.whiteboard.width, this.whiteboard.height ),
          sourceWidth = whiteboardDimensions.width,
          sourceHeight = whiteboardDimensions.height;

      this.whiteboardCtx.drawImage(
        this.image,

        // where on the source image to draw from
        this.scrollX,
        this.scrollY,
        sourceWidth,
        sourceHeight,

        // where on the local canvas to draw to
        0,
        0,
        this.whiteboard.width,
        this.whiteboard.height
      );

      this.currentlyDrawing = false;
    }.bind(this));
  };

  Whiteboard.prototype.setZoom = function( newZoom ) {
    this.zoomRatio = newZoom;

    // restrict to minimum and maximum zoom levels.
    if ( isNaN(this.zoomRatio) ) { this.zoomRatio = 1; }
    if ( this.zoomRatio < .1 ) { this.zoomRatio = .1; }
    if ( this.zoomRatio > 2 ) { this.zoomRatio = 2; }

    return this.zoomRatio;
  };

  // functions for sending things to the server
  // TODO: this should probably be part of the client itself.

  Whiteboard.prototype.sendDrawEvent = function( pointer, x, y ) {
    var translatedCoordinates = this.translateFromLocalToFullsize( x, y );

    this.messageBus.broadcast( 'draw', {
      pointer: pointer,
      x: translatedCoordinates.x,
      y: translatedCoordinates.y,
      penWidth: this.penWidth,
      penColor: this.penColor
    } );
  };

  Whiteboard.prototype.sendPenUp = function() {
    this.client.send( 'pen_up', {} );
  };

  // event receivers
  // for how we handle incoming things

  // receive pen event from server
  Whiteboard.prototype.handleUpdate = function( messageType, message ) {
    var ctx         = this.imageCtx,
        last        = this.penStatuses[message.userId],
        userId      = message.userId.replace(/[^a-z0-9]/ig, ''),
        userEle     = document.getElementById(userId);
        onscreenCtx = this.whiteboardCtx;

    // set background on this user
    userEle.style.backgroundColor = "#" + message.penColor;

    ctx.fillStyle = "#" + message.penColor;
    // ctx.fillRect( message.x, message.y, 2, 2 );

    ctx.beginPath();
    if ( last ) {
      ctx.moveTo(last.x, last.y);
      ctx.lineTo(message.x, message.y);
      ctx.lineWidth = message.penWidth;
      ctx.strokeStyle = "#" + message.penColor;
      ctx.stroke();
    }

    ctx.arc(message.x, message.y, message.penWidth / 2, 0,2*Math.PI, false);
    ctx.fill();

    this.penStatuses[message.userId] = { x: message.x, y: message.y };

    // render offscreen image to onscreen canvas
    this.redraw();
  };

  // receive pen-up from server
  Whiteboard.prototype.handlePenUp = function( messageType, message ) {
    var userId = message.userId
        userEleId = message.userId.replace(/[^a-z0-9]/ig, ''),
        userEle = document.getElementById(userEleId);

    userEle.style.backgroundColor = "transparent";

    delete this.penStatuses[userId];
  };

  Whiteboard.prototype.handlePinchToZoom = function( touches ) {

    // calculate zoomRatio by using the box formed by t1 and t2
    // the ratio is the local size / fullsize size
    var vt1        = { x: touches[0].clientX, y: touches[0].clientY },                  // t1, view scale
        vt2        = { x: touches[1].clientX, y: touches[1].clientY },                  // t2, view scale
        ft1        = this.translateFromLocalToFullsize( vt1.x, vt1.y ),                 // t1 full scale
        ft2        = this.translateFromLocalToFullsize( vt2.x, vt2.y ),                 // t2 full scale
        oft1       = this.zoomTouches[0],                                               // original t1, full scale
        oft2       = this.zoomTouches[1],                                               // original t2, full scale
        vboxWidth  = Math.abs( vt1.x - vt2.x ),                                         // width of the box at screen size
        vboxHeight = Math.abs( vt1.y - vt2.y ),                                         // height of box at screen size
        fboxWidth  = Math.abs( oft1.x - oft2.x ),                                       // width of original box at fullsize
        fboxHeight = Math.abs( oft1.y - oft2.y ),                                       // height of original box at full size
        vboxDiag   = Math.sqrt( Math.pow( vboxWidth, 2 ) + Math.pow( vboxHeight, 2 ) ), // distance between screen-size points
        fboxDiag   = Math.sqrt( Math.pow( fboxWidth, 2 ) + Math.pow( fboxHeight, 2 ) ), // distance between original fullsize points
        zoomRatio  = this.setZoom( vboxDiag / fboxDiag ),                               // ratio of screen size to full size (calculate zoom)

        // scroll values:
        // we have original x/y on fullsize
        // lock touchpoint 1's X from original location to that point on the screen
        dx         = oft1.x - ft1.x,
        dy         = oft1.y - ft1.y;

    this.scroll( dx, dy, true );
  };

  // interaction event handers
  // this is stuff like mouse-move, touch events and keyboard events
  // before they get filtered through our internal API.

  Whiteboard.prototype.handleTouchStart = function( event ) {
    event.preventDefault();

    if ( event.touches.length == 2 ) {
      // if there are 2 fingers down, let's save the 2 points that we're at.
      this.zoomTouches = [
        this.translateFromLocalToFullsize( event.touches[0].clientX, event.touches[0].clientY ),
        this.translateFromLocalToFullsize( event.touches[1].clientX, event.touches[1].clientY )
      ];
    }

  };

  Whiteboard.prototype.handleTouchEnd = function( event ) {
    event.preventDefault();

    if ( event.touches.length < 2 ) {
      // there aren't 2 fingers down, clear the cached zoom stuff
      this.zoomTouches = null;
    }

    if ( event.touches.length == 0 ) {
      this.sendPenUp();
    }

  };

  Whiteboard.prototype.handleTouchMove = function( event ) {
    event.preventDefault();

    var touch = event.changedTouches[0];

    if ( event.touches.length == 1 ) {
      this.sendDrawEvent( 'touch', touch.clientX, touch.clientY );
    } else if ( event.touches.length == 2 ) {
      this.handlePinchToZoom( event.touches );
    }
  };

  Whiteboard.prototype.handleMouseDown = function( event ) {
    event.preventDefault();

    this.penDown = true;

    var x = event.clientX,
        y = event.clientY;

    this.sendDrawEvent( 'mouse', x, y );
  };

  Whiteboard.prototype.handleMouseUp = function( event ) {
    event.preventDefault();

    this.penDown = false;

    this.sendPenUp();
  };

  Whiteboard.prototype.handleMouseMove = function( event ) {
    event.preventDefault();

    // if the mouse isn't down, it's not a drag event.
    if ( ! this.penDown ) { return; }

    var x = event.clientX,
        y = event.clientY;

    this.sendDrawEvent( 'mouse', x, y );
  };

  window.Whiteboard = Whiteboard;

})( window, document );
<|endoftext|>


<commit_before>'use strict';
var fs = require('fs-extra'),
    path = require('path'),
    outDir = require('path').join(__dirname, './../.out'),
    chai = require('chai'),
    expect = require('chai').expect;
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
// long stack for q
require('q').longStackSupport = true;
var disk = require('../lib').slice(-1)[0](
    require('base.logger'),
    {
        baseDir: outDir
    }
);
describe('filestore.disk', function () {
    before(function (done) {
        fs.ensureDir(outDir, done);
    });
    after(function (done) {
        fs.remove(outDir, done);
    });
    // it should return a module
    it('should return a module', function () {
        expect(disk).not.to.equal(undefined);
    });
    // it should be able to create and read an empty folder
    it('should be able to create and read an empty folder', function () {
        return disk.create('folder-one').then(function () {
            return expect(disk.read('folder-one')).to.eventually.become([]);
        });
    });
    // it should be able to create and read a folder with contents
    it('should be able to create and read a folder with contents', function () {
        return disk.create('folder-two/one/text1.txt', 'hello romeo').then(function () {
            return disk.read('folder-two');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            /*jshint -W030 */
            expect(data[0].modified).to.eql(data[0].created);
            expect(data[0].name).to.equal('one');
            expect(data[0].path).to.equal(path.join('folder-two', 'one'));
            expect(data[0].type).to.equal('D');
            return disk.read('folder-two/one');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('.txt');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            /*jshint -W030 */
            expect(data[0].modified).to.eql(data[0].created);
            expect(data[0].name).to.equal('text1.txt');
            expect(data[0].path).to.equal(path.join('folder-two', 'one', 'text1.txt'));
            expect(data[0].type).to.equal('F');
            expect(data[0].size).to.not.equal(0);
        });
    });
    // it should be able to create and read a file with contents
    it('should be able to create and read a file with contents', function () {
        return disk.create('folder-three/one/text1.txt', 'hello romeo').then(function () {
            return expect(disk.read('folder-three/one/text1.txt', 'utf8')).to.eventually.become('hello romeo');
        });
    });
    // it should be able to create and read symbolic links
    it('should be able to create and read symbolic links', function () {
        return disk.create('folder-four/one/text1.txt', 'hello romeo').then(function () {
            return disk.link('folder-four/one', 'folder-five/ten');
        }).then(function () {
            return disk.read('folder-five/ten');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('.txt');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            /*jshint -W030 */
            expect(data[0].modified).to.eql(data[0].created);
            expect(data[0].name).to.equal('text1.txt');
            expect(data[0].path).to.equal(path.join('folder-five', 'ten', 'text1.txt'));
            expect(data[0].type).to.equal('F');
            expect(data[0].size).to.not.equal(0);
        });
    });
    // it should be able to rename files and folders
    it('should be able to rename files and folders', function () {
        return disk.create('folder-rename/one/text1.txt', 'hello romeo').then(function () {
            return disk.rename('folder-rename/one', 'folder-renamed/ten');
        }).then(function () {
            return disk.rename('folder-renamed/ten/text1.txt', 'folder-renamed/ten/22.txt');
        }).then(function () {
            return disk.read('folder-renamed/ten');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('.txt');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            /*jshint -W030 */
            expect(data[0].modified).to.eql(data[0].created);
            expect(data[0].name).to.equal('22.txt');
            expect(data[0].path).to.equal(path.join('folder-renamed', 'ten', '22.txt'));
            expect(data[0].type).to.equal('F');
            expect(data[0].size).to.not.equal(0);
        });
    });
});
<commit_msg>fixed tests<commit_after>'use strict';
var fs = require('fs-extra'),
    path = require('path'),
    outDir = require('path').join(__dirname, './../.out'),
    chai = require('chai'),
    expect = require('chai').expect;
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
// long stack for q
require('q').longStackSupport = true;
var disk = require('../lib').slice(-1)[0](
    require('base.logger'),
    {
        baseDir: outDir
    }
);
describe('filestore.disk', function () {
    before(function (done) {
        fs.ensureDir(outDir, done);
    });
    after(function (done) {
        fs.remove(outDir, done);
    });
    // it should return a module
    it('should return a module', function () {
        expect(disk).not.to.equal(undefined);
    });
    // it should be able to create and read an empty folder
    it('should be able to create and read an empty folder', function () {
        return disk.create('folder-one').then(function () {
            return expect(disk.read('folder-one')).to.eventually.become([]);
        });
    });
    // it should be able to create and read a folder with contents
    it('should be able to create and read a folder with contents', function () {
        return disk.create('folder-two/one/text1.txt', 'hello romeo').then(function () {
            return disk.read('folder-two');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            expect(data[0].modified).to.exist;
            /*jshint -W030 */
            expect(data[0].name).to.equal('one');
            expect(data[0].path).to.equal(path.join('folder-two', 'one'));
            expect(data[0].type).to.equal('D');
            return disk.read('folder-two/one');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('.txt');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            expect(data[0].modified).to.exist;
            /*jshint -W030 */
            expect(data[0].name).to.equal('text1.txt');
            expect(data[0].path).to.equal(path.join('folder-two', 'one', 'text1.txt'));
            expect(data[0].type).to.equal('F');
            expect(data[0].size).to.not.equal(0);
        });
    });
    // it should be able to create and read a file with contents
    it('should be able to create and read a file with contents', function () {
        return disk.create('folder-three/one/text1.txt', 'hello romeo').then(function () {
            return expect(disk.read('folder-three/one/text1.txt', 'utf8')).to.eventually.become('hello romeo');
        });
    });
    // it should be able to create and read symbolic links
    it('should be able to create and read symbolic links', function () {
        return disk.create('folder-four/one/text1.txt', 'hello romeo').then(function () {
            return disk.link('folder-four/one', 'folder-five/ten');
        }).then(function () {
            return disk.read('folder-five/ten');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('.txt');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            expect(data[0].modified).to.exist;
            /*jshint -W030 */
            expect(data[0].name).to.equal('text1.txt');
            expect(data[0].path).to.equal(path.join('folder-five', 'ten', 'text1.txt'));
            expect(data[0].type).to.equal('F');
            expect(data[0].size).to.not.equal(0);
        });
    });
    // it should be able to rename files and folders
    it('should be able to rename files and folders', function () {
        return disk.create('folder-rename/one/text1.txt', 'hello romeo').then(function () {
            return disk.rename('folder-rename/one', 'folder-renamed/ten');
        }).then(function () {
            return disk.rename('folder-renamed/ten/text1.txt', 'folder-renamed/ten/22.txt');
        }).then(function () {
            return disk.read('folder-renamed/ten');
        }).then(function (data) {
            expect(data.length).to.equal(1);
            expect(data[0].extension).to.equal('.txt');
            /*jshint -W030 */
            expect(data[0].created).to.exist;
            expect(data[0].modified).to.exist;
            /*jshint -W030 */
            expect(data[0].name).to.equal('22.txt');
            expect(data[0].path).to.equal(path.join('folder-renamed', 'ten', '22.txt'));
            expect(data[0].type).to.equal('F');
            expect(data[0].size).to.not.equal(0);
        });
    });
});
<|endoftext|>


<commit_before>// Licensed GNU LGPL v2.1 or later: http://www.gnu.org/licenses/lgpl.html
const Electron = require ('electron');

// create the main ebeast window
var win;
function create_window ()
{
  // avoid menu flicker, leave menu construction to the window
  Electron.Menu.setApplicationMenu (null);
  // window configuraiton
  const options = {
    width: 1820, height: 1024, // calling win.maximize() flickers, using a big size not
    webPreferences: {
      defaultEncoding:	'UTF-8',
      defaultFontSize:   	15,
      defaultMonospaceFontSize:	14,
      defaultFontFamily: {
	standard:	'Candara',	// 'Times New Roman',
	serif:		'Constantia',	// 'Times New Roman',
	sansSerif:	'Candara',	// 'Arial',
	monospace:	'Consolas',	// 'Courier New',
	cursive:	'Script',	// 'Script',
	fantasy:	'Impact',	// 'Impact',
      },
    },
    show: false, // avoid incremental load effect, see 'ready-to-show'
    darkTheme: true,
    backgroundColor: '#333333',
  };
  win = new Electron.BrowserWindow (options);
  win.once ('ready-to-show', () => { win.show(); });
  win.loadURL ('file:///' + __dirname + '/index.html');
  // win.webContents.openDevTools();
  win.on ('closed', () => { win = null; });
}
Electron.app.on ('ready', create_window); // create window once everything is loaded

// quit when all windows are closed.
Electron.app.on ('window-all-closed', Electron.app.quit);
<commit_msg>EBEAST: pick BrowserWindow defaults from a BrowserWindowDefaults CSS rule<commit_after>// Licensed GNU LGPL v2.1 or later: http://www.gnu.org/licenses/lgpl.html
const Electron = require ('electron');

// create the main ebeast window
var win;
function create_window ()
{
  const FS = require ('fs');
  // avoid menu flicker, leave menu construction to the window
  Electron.Menu.setApplicationMenu (null);
  // find some CSS presets needed as BrowserWindow defaults
  let appcss = FS.readFileSync ('objects/app.css', 'UTF-8');
  let rex = /\bBrowserWindowDefaults\s*{([^}]*)}/m;
  appcss = rex.exec (appcss)[1];       // extract BrowserWindowDefaults section
  rex = /\/\*([^*]|\*[^\/])*\*\//;
  appcss = appcss.replace (rex, ' ');  // remove comments
  rex = /\bbackgroundColor\s*:\s*([^;}]*)\s*[;}]/mi;
  const backgroundColor = rex.exec (appcss)[1];
  rex = /\bdefaultFontSize\s*:\s*([^;}]*)\s*[;}]/mi;
  const defaultFontSize = rex.exec (appcss)[1];
  rex = /\bdefaultMonospaceFontSize\s*:\s*([^;}]*)\s*[;}]/mi;
  const defaultMonospaceFontSize = rex.exec (appcss)[1];
  // window configuraiton
  const options = {
    width: 				1820, // calling win.maximize() causes flicker
    height: 				1365, // using a big initial size avoids flickering
    backgroundColor: 			backgroundColor,
    webPreferences: {
      defaultEncoding:			'UTF-8',
      defaultFontSize:			parseInt (defaultFontSize),
      defaultMonospaceFontSize:		parseInt (defaultMonospaceFontSize),
      defaultFontFamily: {
	standard:	'sans',		// 'Times New Roman',
	serif:		'Constantia',	// 'Times New Roman',
	sansSerif:	'Candara',	// 'Arial',
	monospace:	'Consolas',	// 'Courier New',
	cursive:	'Script',	// 'Script',
	fantasy:	'Impact',	// 'Impact',
      },
    },
    show: false, // avoid incremental load effect, see 'ready-to-show'
    darkTheme: true,
  };
  win = new Electron.BrowserWindow (options);
  win.once ('ready-to-show', () => { win.show(); });
  win.loadURL ('file:///' + __dirname + '/index.html');
  // win.webContents.openDevTools();
  win.on ('closed', () => { win = null; });
}
Electron.app.on ('ready', create_window); // create window once everything is loaded

// quit when all windows are closed.
Electron.app.on ('window-all-closed', Electron.app.quit);
<|endoftext|>