<commit_before>'use strict';

const defs  = require('defs'),
      Task  = require('Task'),
      Tower = require('Tower');

/**
 * Manages an individual Room
 */
class RoomManager {
    constructor(room) {
        if( !room instanceof RoomManager.expectedClass ) {
            throw new TypeError('Class RoomManager was passed a non Room instance');
        }

        this.me = room;

        if( _.isUndefined(this.me.memory.taskList) ) {
            this.me.memory.taskList = [];
        }
        if( _.isUndefined(this.me.memory.towers) ) {
            this.me.memory.towers = [];
        }

        this.towers = this.me.memory.towers;

        this.taskList = _.clone(this.me.memory.taskList);
    }

    static get expectedClass() {
        return Room;
    }

    runRoom() {
        if( Game.time % defs.ROOM_UPDATE_RATE === 0 ) {
            this.updateRoom();
        }

        this.updateTasks();

        if( this.runTowers() !== OK ) {
            this.updateTowers();
            this.runTowers();
        }
    }

    runTowers() {
        // console.log('runTowers on',JSON.stringify(this.towers));
        for( const id of this.towers ) {
            let tower;
            try {
                tower = new Tower(Game.getObjectById(id));
                tower.doNextTask();
            } catch( ex ) {
                if( ex instanceof TypeError ) {
                    console.log('Error while running towers!', ex);
                    return ERR_INVALID_TARGET;
                } else {
                    throw ex;
                }
            }
        }

        return OK;
    }

    pruneTasks() {
        let len = this.taskList.length;
        this.taskList = _.filter(this.taskList, 'status', Task.STATUS.TODO);
        if( this.taskList.length !== len ) {
            this.me.memory.taskList = this.taskList;
        }
    }

    updateTasks() {
        let foundNewTasks;

        this.pruneTasks();
        foundNewTasks = this.findHostiles();

        if( Game.time % defs.ROOM_UPDATE_REPAIR_RATE === 0 || !this.taskList.find(t => {
                return t.taskType === defs.TASKS.REPAIR;
            }) ) {
            foundNewTasks = this.findRepairTasks();
        }

        // Write to memory. Avoid if there's nothing new.
        if( foundNewTasks ) {
            this.me.memory.taskList = this.taskList = _.sortBy(this.taskList, 'prio');
        }
    }

    /**
     * Updates various parts of the room in memory.
     * Do expensive operations here. Also for things that don't need to run often, like updating structure refs
     */
    updateRoom() {
        this.updateTowers();

        // purge tasks
        this.taskList = [];
    }

    /**
     * Scans the room for our towers and stores them in the room's memory.
     */
    updateTowers() {
        let towers = this.me.find(FIND_MY_STRUCTURES, {
            filter: (s) => {
                return s.structureType === STRUCTURE_TOWER;
            }
        });

        this.towers.length = 0;

        for( const t of towers ) {
            this.towers.push(t.id);
        }
    }

    addToTaskList(t) {
        let foundNewTasks = false;
        if( !_.some(this.taskList, t) ) {
            console.log('Adding task', JSON.stringify(t, 1), 'to taskList', JSON.stringify(this.taskList, 1));
            this.taskList.push(t);
            foundNewTasks = true;
        }

        return foundNewTasks;
    }

    findHostiles() {
        let foundNewTasks = false,
            hostiles      = this.me.find(FIND_HOSTILE_CREEPS);

        if( hostiles.length ) {
            // First partition = hostile fighters
            // Second partition = other hostiles
            let enemies = _.partition(hostiles, (c) => {
                console.log('Looking at hostile creep', JSON.stringify(c));
                return c.getActiveBodyparts(ATTACK) || c.getActiveBodyparts(RANGED_ATTACK) || c.getActiveBodyparts(HEAL);
            });

            console.log('Enemy fighters:', enemies[0].length);
            console.log('Other enemy creeps:', enemies[1].length);

            enemies[0].forEach(enemy => {
                foundNewTasks = foundNewTasks ||
                                this.addToTaskList(Task.toMemoryObject(defs.TASKS.ATTACK, enemy.id, Task.STATUS.TODO, Task.PRIORITY.IMMEDIATE));
            });
            enemies[1].forEach(enemy => {
                foundNewTasks = foundNewTasks || this.addToTaskList(Task.toMemoryObject(defs.TASKS.ATTACK, enemy.id, Task.STATUS.TODO, Task.PRIORITY.URGENT));
            });
        }

        return foundNewTasks;
    }

    findRepairTasks() {
        let repairTargets = this.me.find(FIND_STRUCTURES, {
            filter: (s) => {
                let sType = s.structureType;

                // Don't repair owned structures that aren't ours.
                if( s instanceof OwnedStructure && !s.my ) {
                    // console.log("Not my structure: " + sType + " " + s.id + " " + s.my);
                    return false;
                }

                switch( sType ) {
                case STRUCTURE_SPAWN:
                case STRUCTURE_EXTENSION:
                    return s.hits < s.hitsMax;
                case STRUCTURE_WALL:
                case STRUCTURE_RAMPART:
                    return s.hits < defs.WALL_BUILD_FIX;
                default:
                    return s.hits < s.hitsMax / 2;
                }
            }
        });

        // Create tasks with priority and place them task list
        let foundNewTasks = false;
        repairTargets.forEach(target => {
            let prio;
            switch( target.structureType ) {
            case STRUCTURE_SPAWN:
            case STRUCTURE_EXTENSION:
                prio = Task.PRIORITY.URGENT;
                break;
            case STRUCTURE_WALL:
            case STRUCTURE_RAMPART:
                prio = Task.PRIORITY.HIGH;
                break;
            default:
                prio = Task.PRIORITY.LOW;
                break;
            }

            let newTask = Task.toMemoryObject(defs.TASKS.REPAIR, target.id, Task.STATUS.TODO, prio);
            console.log('For target', target.structureType, ', adding new Task to memory:', JSON.stringify(newTask));
            foundNewTasks = foundNewTasks || this.addToTaskList(newTask);
        });

        return foundNewTasks;
    }

}

module.exports = RoomManager;<commit_msg>room info in memory<commit_after>'use strict';

const defs  = require('defs'),
      Task  = require('Task'),
      Tower = require('Tower');

/**
 * Manages an individual Room
 */
class RoomManager {
    constructor(room) {
        if( !room instanceof RoomManager.expectedClass ) {
            throw new TypeError('Class RoomManager was passed a non Room instance');
        }

        this.me = room;

        if( _.isUndefined(this.me.memory.info) ) {
            this.me.memory.info = RoomManager.getRoomInfo(this.me);
        }
        if( _.isUndefined(this.me.memory.taskList) ) {
            this.me.memory.taskList = [];
        }
        if( _.isUndefined(this.me.memory.towers) ) {
            this.me.memory.towers = [];
        }

        this.towers = this.me.memory.towers;
        this.taskList = _.clone(this.me.memory.taskList);
    }

    static get expectedClass() {
        return Room;
    }

    runRoom() {
        if( Game.time % defs.ROOM_UPDATE_RATE === 0 ) {
            this.updateRoom();
        }

        this.updateTasks();

        if( this.runTowers() !== OK ) {
            this.updateTowers();
            this.runTowers();
        }
    }

    runTowers() {
        // console.log('runTowers on',JSON.stringify(this.towers));
        for( const id of this.towers ) {
            let tower;
            try {
                tower = new Tower(Game.getObjectById(id));
                tower.doNextTask();
            } catch( ex ) {
                if( ex instanceof TypeError ) {
                    console.log('Error while running towers!', ex);
                    return ERR_INVALID_TARGET;
                } else {
                    throw ex;
                }
            }
        }

        return OK;
    }

    pruneTasks() {
        let len = this.taskList.length;
        this.taskList = _.filter(this.taskList, 'status', Task.STATUS.TODO);
        if( this.taskList.length !== len ) {
            this.me.memory.taskList = this.taskList;
        }
    }

    updateTasks() {
        let foundNewTasks;

        this.pruneTasks();
        foundNewTasks = this.findHostiles();

        if( Game.time % defs.ROOM_UPDATE_REPAIR_RATE === 0 || !this.taskList.find(t => {
                return t.taskType === defs.TASKS.REPAIR;
            }) ) {
            foundNewTasks = this.findRepairTasks();
        }

        // Write to memory. Avoid if there's nothing new.
        if( foundNewTasks ) {
            this.me.memory.taskList = this.taskList = _.sortBy(this.taskList, 'prio');
        }
    }

    /**
     * Updates various parts of the room in memory.
     * Do expensive operations here. Also for things that don't need to run often, like updating structure refs
     */
    updateRoom() {
        this.updateTowers();

        // purge tasks
        this.taskList = [];
    }

    /**
     * Scans the room for our towers and stores them in the room's memory.
     */
    updateTowers() {
        let towers = this.me.find(FIND_MY_STRUCTURES, {
            filter: (s) => {
                return s.structureType === STRUCTURE_TOWER;
            }
        });

        this.towers.length = 0;

        for( const t of towers ) {
            this.towers.push(t.id);
        }
    }

    addToTaskList(t) {
        let foundNewTasks = false;
        if( !_.some(this.taskList, t) ) {
            console.log('Adding task', JSON.stringify(t, 1), 'to taskList', JSON.stringify(this.taskList, 1));
            this.taskList.push(t);
            foundNewTasks = true;
        }

        return foundNewTasks;
    }

    findHostiles() {
        let foundNewTasks = false,
            hostiles      = this.me.find(FIND_HOSTILE_CREEPS);

        if( hostiles.length ) {
            // First partition = hostile fighters
            // Second partition = other hostiles
            let enemies = _.partition(hostiles, (c) => {
                console.log('Looking at hostile creep', JSON.stringify(c));
                return c.getActiveBodyparts(ATTACK) || c.getActiveBodyparts(RANGED_ATTACK) || c.getActiveBodyparts(HEAL);
            });

            console.log('Enemy fighters:', enemies[0].length);
            console.log('Other enemy creeps:', enemies[1].length);

            enemies[0].forEach(enemy => {
                foundNewTasks = foundNewTasks || this.addToTaskList(Task.toMemoryObject(defs.TASKS.ATTACK, enemy.id, Task.STATUS.TODO, Task.PRIORITY.IMMEDIATE));
            });
            enemies[1].forEach(enemy => {
                foundNewTasks = foundNewTasks || this.addToTaskList(Task.toMemoryObject(defs.TASKS.ATTACK, enemy.id, Task.STATUS.TODO, Task.PRIORITY.URGENT));
            });
        }

        return foundNewTasks;
    }

    findRepairTasks() {
        let repairTargets = this.me.find(FIND_STRUCTURES, {
            filter: (s) => {
                let sType = s.structureType;

                // Don't repair owned structures that aren't ours.
                if( s instanceof OwnedStructure && !s.my ) {
                    // console.log("Not my structure: " + sType + " " + s.id + " " + s.my);
                    return false;
                }

                switch( sType ) {
                case STRUCTURE_SPAWN:
                case STRUCTURE_EXTENSION:
                    return s.hits < s.hitsMax;
                case STRUCTURE_WALL:
                case STRUCTURE_RAMPART:
                    return s.hits < defs.WALL_BUILD_FIX;
                default:
                    return s.hits < s.hitsMax / 2;
                }
            }
        });

        // Create tasks with priority and place them task list
        let foundNewTasks = false;
        repairTargets.forEach(target => {
            let prio;
            switch( target.structureType ) {
            case STRUCTURE_SPAWN:
            case STRUCTURE_EXTENSION:
                prio = Task.PRIORITY.URGENT;
                break;
            case STRUCTURE_WALL:
            case STRUCTURE_RAMPART:
                prio = Task.PRIORITY.HIGH;
                break;
            default:
                prio = Task.PRIORITY.LOW;
                break;
            }

            let newTask = Task.toMemoryObject(defs.TASKS.REPAIR, target.id, Task.STATUS.TODO, prio);
            console.log('For target', target.structureType, ', adding new Task to memory:', JSON.stringify(newTask));
            foundNewTasks = foundNewTasks || this.addToTaskList(newTask);
        });

        return foundNewTasks;
    }

    static getRoomInfo(room) {
        let info = {},
            sources = room.find(FIND_SOURCES),
            minerals = room.find(FIND_MINERALS);

        if( sources.length ) {
            info.sources = {};
            for(const s of sources) {
                info.sources[s.id] = {x: s.pos.x, y: s.pos.y};
            }
        }

        if( minerals.length ) {
            info.minerals = {};
            for(const m of minerals) {
                info.minerals[m.id] = {x: m.pos.x, y: m.pos.y};
            }
        }

        info.exits = Game.map.describeExits(room.name);

        return info;
    }
}

module.exports = RoomManager;<|endoftext|>


<commit_before>define(['js/core/Bindable'], function (Bindable) {
    var Validator = Bindable.inherit('js.data.validator.Validator', {

        $validatorCache: {},

        defaults: {
            field: null,
            errorCode: 'isInvalid',
            errorMessage: null
        },

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

            var cacheKey = this._generateCacheKey.apply(this, arguments);
            if (cacheKey && !this.$validatorCache[cacheKey]) {
                this.$validatorCache[cacheKey] = this;
            }

            return this.$validatorCache[cacheKey];
        },

        _generateCacheKey: function () {
            return null;
        },

        /***
         * validates entities
         * @param entity
         * @param callback
         */
        validate: function (entity, callback) {

            var self = this,
                callbackInvoked = false;

            try {
                internalCallback(null, this._validate(entity));
            } catch(e) {
                if (e instanceof Validator.Error) {
                    internalCallback(null, e);
                } else{
                    internalCallback(e);
                }
            }

            function internalCallback(err, result) {
                if (callbackInvoked) {
                    self.log('Validator returned twice. Ignore second return', Base.LOGLEVEL.WARN);
                    return;
                }

                callbackInvoked = true;
                callback(err, result);
            }
        },

        /***
         * performs a synchronous validation
         * @param {js.data.Entity} entity
         * @abstract
         * @private
         */
        _validate: function (entity) {
            return "abstract method _validate from Validator";
        },

        _getErrorMessage: function () {
            if(!this.$.errorMessage){
                if(this.$.field){
                    return "Field '%' is invalid".replace('%', this.$.field);
                }else{
                    return "Entity is invalid";
                }
            }
            return this.$.errorMessage;
        },
        _createFieldError: function () {
            return new Validator.Error({
                code: this.$.errorCode,
                message: this._getErrorMessage(),
                field: this.$.field
            })
        }

    });

    Validator.Error = Bindable.inherit({

    });

    return Validator;
})
;<commit_msg>fixed error message formatting<commit_after>define(['js/core/Bindable'], function (Bindable) {
    var Validator = Bindable.inherit('js.data.validator.Validator', {

        $validatorCache: {},

        defaults: {
            field: null,
            errorCode: 'isInvalid',
            errorMessage: null
        },

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

            var cacheKey = this._generateCacheKey.apply(this, arguments);
            if (cacheKey && !this.$validatorCache[cacheKey]) {
                this.$validatorCache[cacheKey] = this;
            }

            return this.$validatorCache[cacheKey];
        },

        _generateCacheKey: function () {
            return null;
        },

        /***
         * validates entities
         * @param entity
         * @param callback
         */
        validate: function (entity, callback) {

            var self = this,
                callbackInvoked = false;

            try {
                internalCallback(null, this._validate(entity));
            } catch(e) {
                if (e instanceof Validator.Error) {
                    internalCallback(null, e);
                } else {
                    internalCallback(e);
                }
            }

            function internalCallback(err, result) {
                if (callbackInvoked) {
                    self.log('Validator returned twice. Ignore second return', Base.LOGLEVEL.WARN);
                    return;
                }

                callbackInvoked = true;
                callback(err, result);
            }
        },

        /***
         * performs a synchronous validation
         * @param {js.data.Entity} entity
         * @abstract
         * @private
         */
        _validate: function (entity) {
            return "abstract method _validate from Validator";
        },

        _getErrorMessage: function () {
            if (this.$.field) {
                return  (this.$.errorMessage || "Field '%' is invalid").replace('%', this.$.field);
            } else {
                return this.$.errorMessage || "Entity is invalid";
            }
        },
        _createFieldError: function () {
            return new Validator.Error({
                code: this.$.errorCode,
                message: this._getErrorMessage(),
                field: this.$.field
            })
        }

    });

    Validator.Error = Bindable.inherit({

    });

    return Validator;
})
;<|endoftext|>


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

/**
 * A class for the fan, which is an energy user
 *
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var EnergyChunkPathMover = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyChunkPathMover' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  var EnergyUser = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyUser' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Property = require( 'AXON/Property' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var VELOCITY_DIVISOR = 3; // empirically determined, lower number = faster fan speed
  var RADIATED_ENERGY_CHUNK_TRAVEL_DISTANCE = 0.2; // in meters
  var INSIDE_FAN_ENERGY_CHUNK_TRAVEL_DISTANCE = 0.05; // in meters
  var BLOWN_ENERGY_CHUNK_TRAVEL_DISTANCE = 0.12; // in meters
  var BLOWN_ENERGY_CHUNK_VELOCITY = EFACConstants.ENERGY_CHUNK_VELOCITY * 2; // empirically determined
  var CHANCE_ENERGY_LOST_TO_MOTOR_HEAT = 0.1;

  // energy chunk path offsets
  var OFFSET_TO_FIRST_WIRE_CURVE_POINT = new Vector2( -0.035, -0.0375 );
  var OFFSET_TO_SECOND_WIRE_CURVE_POINT = new Vector2( -0.026, -0.025 );
  var OFFSET_TO_THIRD_WIRE_CURVE_POINT = new Vector2( -0.025, -0.0175 );
  var OFFSET_TO_BOTTOM_OF_SECOND_WIRE_BEND = new Vector2( -0.025, 0.011 );
  var OFFSET_TO_FOURTH_WIRE_CURVE_POINT = new Vector2( -0.0225, 0.02 );
  var OFFSET_TO_FIFTH_WIRE_CURVE_POINT = new Vector2( -0.0125, 0.031 );
  var OFFSET_TO_SIXTH_WIRE_CURVE_POINT = new Vector2( -0.005, 0.033 );
  var OFFSET_TO_FAN_MOTOR_INTERIOR = new Vector2( 0.005, 0.033 );

  // images
  var FAN_ICON = require( 'image!ENERGY_FORMS_AND_CHANGES/fan_icon.png' );

  /**
   * @param {Property.<boolean>} energyChunksVisibleProperty
   * @constructor
   */
  function Fan( energyChunksVisibleProperty ) {

    EnergyUser.call( this, new Image( FAN_ICON ) );

    // @public (read-only) {NumberProperty}
    this.bladePositionProperty = new Property( 0 );

    // @private
    this.energyChunksVisibleProperty = energyChunksVisibleProperty;

    // @private - movers that control how the energy chunks move towards and through the fan
    this.electricalEnergyChunkMovers = [];
    this.radiatedEnergyChunkMovers = [];
    this.mechanicalEnergyChunkMovers = [];
  }

  energyFormsAndChanges.register( 'Fan', Fan );

  return inherit( EnergyUser, Fan, {

    /**
     * @param {number} dt - time step, in seconds
     * @param {Energy} incomingEnergy
     * @public
     * @override
     */
    step: function( dt, incomingEnergy ) {
      var self = this;
      if ( !this.activeProperty.value ) {
        return;
      }

      // handle any incoming energy chunks
      if ( this.incomingEnergyChunks.length > 0 ) {
        this.incomingEnergyChunks.forEach( function( chunk ) {

          assert && assert(
            chunk.energyTypeProperty.value === EnergyType.ELECTRICAL,
            'Energy chunk type should be ELECTRICAL but is ' + chunk.energyTypeProperty.value
          );

          // add the energy chunk to the list of those under management
          self.energyChunkList.push( chunk );

          // add a "mover" that will move this energy chunk through the wire to the heating element
          self.electricalEnergyChunkMovers.push( new EnergyChunkPathMover( chunk,
            self.createElectricalEnergyChunkPath( self.positionProperty.get() ),
            EFACConstants.ENERGY_CHUNK_VELOCITY ) );
        } );

        // clear incoming chunks array
        this.incomingEnergyChunks.length = 0;
      }
      this.moveElectricalEnergyChunks( dt );
      this.moveRadiatedEnergyChunks( dt );
      this.moveBlownEnergyChunks( dt );

      // set how fast the fan is turning
      if ( this.energyChunksVisibleProperty.get() ) {
        // handle case where fan only turns when energy chunks get to it
      }
      else {
        var angularVelocity = incomingEnergy.amount / VELOCITY_DIVISOR;
        var newAngle = ( this.bladePositionProperty.value + angularVelocity * dt ) % ( 2 * Math.PI );
        this.bladePositionProperty.set( newAngle );
      }
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveElectricalEnergyChunks: function( dt ) {
      var self = this;
      var movers = _.clone( this.electricalEnergyChunkMovers );

      movers.forEach( function( mover ) {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          // the electrical energy chunk has reached the motor, so it needs to change into mechanical or thermal energy
          _.pull( self.electricalEnergyChunkMovers, mover );

          if ( phet.joist.random.nextDouble() > CHANCE_ENERGY_LOST_TO_MOTOR_HEAT ) {
            mover.energyChunk.energyTypeProperty.set( EnergyType.MECHANICAL );

            // release the energy chunk as mechanical to blow away
            self.mechanicalEnergyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk,
              self.createBlownEnergyChunkPath( mover.energyChunk.positionProperty.get() ),
              BLOWN_ENERGY_CHUNK_VELOCITY ) );
          }
          else {
            mover.energyChunk.energyTypeProperty.set( EnergyType.THERMAL );

            // release the energy chunk as thermal to radiate away
            self.radiatedEnergyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk,
              self.createRadiatedEnergyChunkPath( mover.energyChunk.positionProperty.get() ),
              EFACConstants.ENERGY_CHUNK_VELOCITY ) );
          }
        }
      } );
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveRadiatedEnergyChunks: function( dt ) {
      var self = this;
      var movers = _.clone( this.radiatedEnergyChunkMovers );

      movers.forEach( function( mover ) {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          // remove this energy chunk entirely
          _.pull( self.radiatedEnergyChunkMovers, mover );
        }
      } );
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveBlownEnergyChunks: function( dt ) {
      var self = this;
      var movers = _.clone( this.mechanicalEnergyChunkMovers );

      movers.forEach( function( mover ) {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          // remove this energy chunk entirely
          _.pull( self.mechanicalEnergyChunkMovers, mover );
        }
      } );
    },

    /**
     * create a path for chunks to follow when traveling along the wire to the motor
     * @param  {Vector2} center
     * @returns {Vector2[]}
     * @private
     */
    createElectricalEnergyChunkPath: function( center ) {
      var path = [];

      path.push( center.plus( OFFSET_TO_FIRST_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_SECOND_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_THIRD_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_BOTTOM_OF_SECOND_WIRE_BEND ) );
      path.push( center.plus( OFFSET_TO_FOURTH_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_FIFTH_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_SIXTH_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_FAN_MOTOR_INTERIOR ) );
      return path;
    },

    /**
     * create a path for chunks to follow when radiated from the motor. originally from BeakerHeater.js
     * @param  {Vector2} startingPoint
     * @returns {Vector2[]}
     * @private
     */
    createRadiatedEnergyChunkPath: function( startingPoint ) {
      var path = [];
      var numDirectionChanges = 4; // Empirically chosen.
      var nominalTravelVector = new Vector2( 0, RADIATED_ENERGY_CHUNK_TRAVEL_DISTANCE / numDirectionChanges );

      // The first point is straight above the starting point.  This is done because it looks good, making the chunk
      // move straight up out of the motor.
      var currentPosition = startingPoint.plus( nominalTravelVector );
      path.push( currentPosition );

      // add the remaining points in the path
      for ( var i = 0; i < numDirectionChanges - 1; i++ ) {
        var movement = nominalTravelVector.rotated( ( phet.joist.random.nextDouble() - 0.5 ) * Math.PI / 4 );
        currentPosition = currentPosition.plus( movement );
        path.push( currentPosition );
      }

      return path;
    },

    /**
     * create a path for chunks to follow when blown out of the fan.
     * @param  {Vector2} startingPoint
     * @returns {Vector2[]}
     * @private
     */
    createBlownEnergyChunkPath: function( startingPoint ) {
      var path = [];
      var numDirectionChanges = 6; // Empirically chosen.
      var nominalTravelVector = new Vector2( BLOWN_ENERGY_CHUNK_TRAVEL_DISTANCE / numDirectionChanges, 0 );

      // The first point is straight right the starting point.  This is done because it makes the chunk
      // move straight out of the fan center cone.
      var currentPosition = startingPoint.plus( new Vector2( INSIDE_FAN_ENERGY_CHUNK_TRAVEL_DISTANCE, 0 ) );
      path.push( currentPosition );

      // add the remaining points in the path
      for ( var i = 0; i < numDirectionChanges - 1; i++ ) {
        var movement = nominalTravelVector.rotated( ( phet.joist.random.nextDouble() - 0.5 ) * Math.PI / 4 );
        currentPosition = currentPosition.plus( movement );
        path.push( currentPosition );
      }

      return path;
    },

    /**
     * restore the initial state
     * @public
     */
    reset: function() {
      this.bladePositionProperty.reset();
    }
  } );
} );

<commit_msg>Fan turns while energy chunks are entering the motor, see https://github.com/phetsims/energy-forms-and-changes/issues/76<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * A class for the fan, which is an energy user
 *
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var EnergyChunkPathMover = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyChunkPathMover' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  var EnergyUser = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/EnergyUser' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Property = require( 'AXON/Property' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var VELOCITY_DIVISOR = 2.7; // empirically determined, lower number = faster fan speed
  var RADIATED_ENERGY_CHUNK_TRAVEL_DISTANCE = 0.2; // in meters
  var INSIDE_FAN_ENERGY_CHUNK_TRAVEL_DISTANCE = 0.05; // in meters
  var BLOWN_ENERGY_CHUNK_TRAVEL_DISTANCE = 0.12; // in meters
  var BLOWN_ENERGY_CHUNK_VELOCITY = EFACConstants.ENERGY_CHUNK_VELOCITY * 2; // empirically determined
  var CHANCE_ENERGY_LOST_TO_MOTOR_HEAT = 0.1; // empirically determined
  var INCOMING_ENERGY_FAN_THRESHOLD = 4; // empirically determined, eliminates last few jumpy frames when fan slows to a stop
  var MAX_INCOMING_ENERGY = 150;

  // energy chunk path offsets
  var OFFSET_TO_FIRST_WIRE_CURVE_POINT = new Vector2( -0.035, -0.0375 );
  var OFFSET_TO_SECOND_WIRE_CURVE_POINT = new Vector2( -0.026, -0.025 );
  var OFFSET_TO_THIRD_WIRE_CURVE_POINT = new Vector2( -0.025, -0.0175 );
  var OFFSET_TO_BOTTOM_OF_SECOND_WIRE_BEND = new Vector2( -0.025, 0.011 );
  var OFFSET_TO_FOURTH_WIRE_CURVE_POINT = new Vector2( -0.0225, 0.02 );
  var OFFSET_TO_FIFTH_WIRE_CURVE_POINT = new Vector2( -0.0125, 0.031 );
  var OFFSET_TO_SIXTH_WIRE_CURVE_POINT = new Vector2( -0.005, 0.034 );
  var OFFSET_TO_FAN_MOTOR_INTERIOR = new Vector2( 0.005, 0.034 );

  // images
  var FAN_ICON = require( 'image!ENERGY_FORMS_AND_CHANGES/fan_icon.png' );

  /**
   * @param {Property.<boolean>} energyChunksVisibleProperty
   * @constructor
   */
  function Fan( energyChunksVisibleProperty ) {

    EnergyUser.call( this, new Image( FAN_ICON ) );

    // @public (read-only) {NumberProperty}
    this.bladePositionProperty = new Property( 0 );

    // @private
    this.energyChunksVisibleProperty = energyChunksVisibleProperty;
    this.energyChunkIncomingEnergy = 0;

    // @private - movers that control how the energy chunks move towards and through the fan
    this.electricalEnergyChunkMovers = [];
    this.radiatedEnergyChunkMovers = [];
    this.mechanicalEnergyChunkMovers = [];
  }

  energyFormsAndChanges.register( 'Fan', Fan );

  return inherit( EnergyUser, Fan, {

    /**
     * @param {number} dt - time step, in seconds
     * @param {Energy} incomingEnergy
     * @public
     * @override
     */
    step: function( dt, incomingEnergy ) {
      var self = this;
      if ( !this.activeProperty.value ) {
        return;
      }

      // handle any incoming energy chunks
      if ( this.incomingEnergyChunks.length > 0 ) {
        this.incomingEnergyChunks.forEach( function( chunk ) {

          assert && assert(
            chunk.energyTypeProperty.value === EnergyType.ELECTRICAL,
            'Energy chunk type should be ELECTRICAL but is ' + chunk.energyTypeProperty.value
          );

          // add the energy chunk to the list of those under management
          self.energyChunkList.push( chunk );

          // add a "mover" that will move this energy chunk through the wire to the heating element
          self.electricalEnergyChunkMovers.push( new EnergyChunkPathMover( chunk,
            self.createElectricalEnergyChunkPath( self.positionProperty.get() ),
            EFACConstants.ENERGY_CHUNK_VELOCITY ) );
        } );

        // clear incoming chunks array
        this.incomingEnergyChunks.length = 0;
      }
      this.moveElectricalEnergyChunks( dt );
      this.moveRadiatedEnergyChunks( dt );
      this.moveBlownEnergyChunks( dt );

      // when energy chunks are not entering the motor, chip away at the remaining energy from the last chunk
      this.energyChunkIncomingEnergy = this.energyChunkIncomingEnergy === 0 ? 0 : this.energyChunkIncomingEnergy - 0.3;

      // set how fast the fan is turning
      if ( this.energyChunksVisibleProperty.get() ) {
        // handle case where fan only turns when energy chunks are getting to the motor
        this.setBladePosition( dt, this.energyChunkIncomingEnergy );
      }
      else {
        this.setBladePosition( dt, incomingEnergy.amount );
      }
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @param {number} incomingEnergyAmount
     * @private
     */
    setBladePosition: function( dt, incomingEnergyAmount ) {
      if ( incomingEnergyAmount > INCOMING_ENERGY_FAN_THRESHOLD ) {
        var angularVelocity = incomingEnergyAmount / VELOCITY_DIVISOR;
        var newAngle = ( this.bladePositionProperty.value + angularVelocity * dt ) % ( 2 * Math.PI );
        this.bladePositionProperty.set( newAngle );
      }
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveElectricalEnergyChunks: function( dt ) {
      var self = this;
      var movers = _.clone( this.electricalEnergyChunkMovers );

      movers.forEach( function( mover ) {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          // the electrical energy chunk has reached the motor, so it needs to change into mechanical or thermal energy
          _.pull( self.electricalEnergyChunkMovers, mover );
          self.hasEnergy = true;

          if ( phet.joist.random.nextDouble() > CHANCE_ENERGY_LOST_TO_MOTOR_HEAT || self.energyChunkIncomingEnergy < MAX_INCOMING_ENERGY / 2 ) {
            mover.energyChunk.energyTypeProperty.set( EnergyType.MECHANICAL );
            self.energyChunkIncomingEnergy = self.energyChunkIncomingEnergy < MAX_INCOMING_ENERGY / 4 ?
                                             self.energyChunkIncomingEnergy = MAX_INCOMING_ENERGY / 2 :
                                             self.energyChunkIncomingEnergy = MAX_INCOMING_ENERGY;

            // release the energy chunk as mechanical to blow away
            self.mechanicalEnergyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk,
              self.createBlownEnergyChunkPath( mover.energyChunk.positionProperty.get() ),
              BLOWN_ENERGY_CHUNK_VELOCITY ) );
          }
          else {
            mover.energyChunk.energyTypeProperty.set( EnergyType.THERMAL );

            // release the energy chunk as thermal to radiate away
            self.radiatedEnergyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk,
              self.createRadiatedEnergyChunkPath( mover.energyChunk.positionProperty.get() ),
              EFACConstants.ENERGY_CHUNK_VELOCITY ) );
          }
        }
      } );
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveRadiatedEnergyChunks: function( dt ) {
      var self = this;
      var movers = _.clone( this.radiatedEnergyChunkMovers );

      movers.forEach( function( mover ) {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          // remove this energy chunk entirely
          _.pull( self.radiatedEnergyChunkMovers, mover );
        }
      } );
    },

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveBlownEnergyChunks: function( dt ) {
      var self = this;
      var movers = _.clone( this.mechanicalEnergyChunkMovers );

      movers.forEach( function( mover ) {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {

          // remove this energy chunk entirely
          _.pull( self.mechanicalEnergyChunkMovers, mover );
        }
      } );
    },

    /**
     * create a path for chunks to follow when traveling along the wire to the motor
     * @param  {Vector2} center
     * @returns {Vector2[]}
     * @private
     */
    createElectricalEnergyChunkPath: function( center ) {
      var path = [];

      path.push( center.plus( OFFSET_TO_FIRST_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_SECOND_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_THIRD_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_BOTTOM_OF_SECOND_WIRE_BEND ) );
      path.push( center.plus( OFFSET_TO_FOURTH_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_FIFTH_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_SIXTH_WIRE_CURVE_POINT ) );
      path.push( center.plus( OFFSET_TO_FAN_MOTOR_INTERIOR ) );
      return path;
    },

    /**
     * create a path for chunks to follow when radiated from the motor. originally from BeakerHeater.js
     * @param  {Vector2} startingPoint
     * @returns {Vector2[]}
     * @private
     */
    createRadiatedEnergyChunkPath: function( startingPoint ) {
      var path = [];
      var numDirectionChanges = 8; // Empirically chosen.
      var nominalTravelVector = new Vector2( 0, RADIATED_ENERGY_CHUNK_TRAVEL_DISTANCE / numDirectionChanges );

      // The first point is straight above the starting point.  This is done because it looks good, making the chunk
      // move straight up out of the motor.
      var currentPosition = startingPoint.plus( nominalTravelVector );
      path.push( currentPosition );

      // add the remaining points in the path
      for ( var i = 0; i < numDirectionChanges - 1; i++ ) {
        var movement = nominalTravelVector.rotated( ( phet.joist.random.nextDouble() - 0.5 ) * Math.PI / 4 );
        currentPosition = currentPosition.plus( movement );
        path.push( currentPosition );
      }

      return path;
    },

    /**
     * create a path for chunks to follow when blown out of the fan.
     * @param  {Vector2} startingPoint
     * @returns {Vector2[]}
     * @private
     */
    createBlownEnergyChunkPath: function( startingPoint ) {
      var path = [];
      var numDirectionChanges = 6; // empirically determined
      var nominalTravelVector = new Vector2( BLOWN_ENERGY_CHUNK_TRAVEL_DISTANCE / numDirectionChanges, 0 );

      // The first point is straight right the starting point.  This is done because it makes the chunk
      // move straight out of the fan center cone.
      var currentPosition = startingPoint.plus( new Vector2( INSIDE_FAN_ENERGY_CHUNK_TRAVEL_DISTANCE, 0 ) );
      path.push( currentPosition );

      // add the remaining points in the path
      for ( var i = 0; i < numDirectionChanges - 1; i++ ) {
        var movement = nominalTravelVector.rotated( ( phet.joist.random.nextDouble() - 0.5 ) * Math.PI / 4 );
        currentPosition = currentPosition.plus( movement );
        path.push( currentPosition );
      }

      return path;
    },

    /**
     * restore the initial state
     * @public
     */
    reset: function() {
      this.bladePositionProperty.reset();
      this.energyChunkIncomingEnergy = 0;
      this.electricalEnergyChunkMovers = [];
      this.radiatedEnergyChunkMovers = [];
      this.mechanicalEnergyChunkMovers = [];
    }
  } );
} );

<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 */

CLASS({
  package: 'foam.demos.empire',
  name: 'Preso2',
  extendsModel: 'foam.flow.Section',

  constants: { ELEMENT_NAME: 'foam-demos-flow' },

  requires: [
    'Timer',
    'com.google.sweeper.Game',
    'foam.demos.InterpolatedClocks',
    'foam.demos.graphics.Dragon',
    'foam.demos.graphics.EyeCView',
    'foam.demos.physics.Bubbles',
    'foam.demos.physics.Baloons',
    'foam.demos.physics.Collision',
    'foam.demos.physics.CollisionWithSpring',
    'foam.demos.physics.Spring',
    'foam.demos.wat.Grid',
    'foam.demos.wat.Slides',
    'foam.documentation.diagram.DocDiagramView',
    'foam.documentation.diagram.DocModelDiagramView',
    'foam.flow.CodeSample',
    'foam.flow.Section',
    'foam.flow.Slides',
    'foam.graphics.Circle',
    'foam.graphics.Gauge',
    'foam.input.touch.GestureManager',
    'foam.input.touch.TouchManager',
    'foam.ui.AlternateView',
    'foam.ui.JSView',
    'foam.ui.SwipeAltView',
    'foam.ui.TableView',
    'foam.ui.ViewChoice',
    'foam.ui.md.SlideStyles',
    'foam.ui.md.TwoPaneView',
    'foam.memento.FragmentMementoMgr'
  ],

  imports: [
    'FOAMWindow'
  ],

  exports: [
    'gestureManager',
    'touchManager'
  ],

  properties: [
    {
      name: 'gestureManager',
      factory: function() {
        var gm = this.GestureManager.create();
        window.X.gestureManager = gm;
        return gm;
      }
    },
    {
      name: 'touchManager',
      factory: function() {
        // TODO(braden): HACK This should be just exporting the property, but
        // the context is not properly passed into views created using <foam>
        // tags right now. Clean up this and gestureManager below.
        var tm = this.TouchManager.create();
        window.X.touchManager = tm;
        return tm;
      }
    }
  ],

  methods: {
    init: function() {
      this.SUPER.apply(this, arguments);

      // Ensure that SlideStyles CSS gets installed.
      this.SlideStyles.create();

      this.X.registerModel(this.Slides, 'foam.flow.Slides');
      this.X.registerModel(this.Grid, 'foam.flow.Grid');

      this.X.registerElement('circle', 'foam.graphics.Circle');

      this.Slides.getProperty('installCSS').documentInstallFn.call(
        this.Slides.getPrototype(), this.X);
      this.Slides.getProperty('registerElement').documentInstallFn.call(
        this.Slides.getPrototype(), this.X);

      this.CodeSample.getProperty('installCSS').documentInstallFn.call(
        this.CodeSample.getPrototype(), this.X);
      this.CodeSample.getProperty('registerElement').documentInstallFn.call(
        this.CodeSample.getPrototype(), this.X);

      this.Section.getProperty('installCSS').documentInstallFn.call(
        this.Section.getPrototype(), this.X);
      this.Section.getProperty('registerElement').documentInstallFn.call(
        this.Section.getPrototype(), this.X);
      this.Slides.getProperty('registerElement').documentInstallFn.call(
          this.Slides.getPrototype(), this.X);
    }
  },

  templates: [
    { name: 'toHTML' },
    // (1) Undo font scaling from foam-components styles.
    // (2) Reposition pong component scores within flex container.
    // (3) Code formatting.
    // (4) Two-pane controller styling.
    // (4) Summary list element element styling.
    function CSS() {/*
      slides deck { position: relative; }
      foam-components { font-size: 100%; }
      #pong-container div { display: flex; }
      #pong-container span[name="lScore"] { left: 390px; top:20px; z-index: 2; }
      #pong-container span[name="rScore"] { left: 490px; top:20px; z-index: 2; }
      .pln { color: #000 }
      .str,
      .atv { color: #0f9d58 }
      .kwd,
      .tag { color: #4285f4;  }
      .com { color: #999 }
      .typ,
      .var { color: #673ab7 }
      .lit { color: #db4437 }
      .pun,
      .opn,
      .clo { color: #a3a3a3 }
      .atn { color: #e91e63 }
      .dec,
      .var { color: #e67c73 }
      .fun { color: #fff }
      .twopane-container { height: 100%; width: 100%; }
      slides .twopane-left {
        min-width: 450px;
        max-width: 40%;
        height: 85%;
      }
      slides .twopane-right {
        background: white;
      }
      li { padding-bottom: 2px; }
      li.d1 { margin-left: 16px; font-size: 30px; }
      li.d2 { margin-left: 32px; font-size: 28px; }
      li.d3 { margin-left: 48px; font-size: 26px; }
      li.d4 { margin-left: 64px; font-size: 24px; }
    */}
  ]
});
<commit_msg>Fixed Preso2 requires Timer to foam.util.Timer.<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 */

CLASS({
  package: 'foam.demos.empire',
  name: 'Preso2',
  extendsModel: 'foam.flow.Section',

  constants: { ELEMENT_NAME: 'foam-demos-flow' },

  requires: [
    'com.google.sweeper.Game',
    'foam.demos.InterpolatedClocks',
    'foam.demos.graphics.Dragon',
    'foam.demos.graphics.EyeCView',
    'foam.demos.physics.Bubbles',
    'foam.demos.physics.Baloons',
    'foam.demos.physics.Collision',
    'foam.demos.physics.CollisionWithSpring',
    'foam.demos.physics.Spring',
    'foam.demos.wat.Grid',
    'foam.demos.wat.Slides',
    'foam.documentation.diagram.DocDiagramView',
    'foam.documentation.diagram.DocModelDiagramView',
    'foam.flow.CodeSample',
    'foam.flow.Section',
    'foam.flow.Slides',
    'foam.graphics.Circle',
    'foam.graphics.Gauge',
    'foam.input.touch.GestureManager',
    'foam.input.touch.TouchManager',
    'foam.ui.AlternateView',
    'foam.ui.JSView',
    'foam.ui.SwipeAltView',
    'foam.ui.TableView',
    'foam.ui.ViewChoice',
    'foam.ui.md.SlideStyles',
    'foam.ui.md.TwoPaneView',
    'foam.util.Timer',
    'foam.memento.FragmentMementoMgr'
  ],

  imports: [
    'FOAMWindow'
  ],

  exports: [
    'gestureManager',
    'touchManager'
  ],

  properties: [
    {
      name: 'gestureManager',
      factory: function() {
        var gm = this.GestureManager.create();
        window.X.gestureManager = gm;
        return gm;
      }
    },
    {
      name: 'touchManager',
      factory: function() {
        // TODO(braden): HACK This should be just exporting the property, but
        // the context is not properly passed into views created using <foam>
        // tags right now. Clean up this and gestureManager below.
        var tm = this.TouchManager.create();
        window.X.touchManager = tm;
        return tm;
      }
    }
  ],

  methods: {
    init: function() {
      this.SUPER.apply(this, arguments);

      // Ensure that SlideStyles CSS gets installed.
      this.SlideStyles.create();

      this.X.registerModel(this.Slides, 'foam.flow.Slides');
      this.X.registerModel(this.Grid, 'foam.flow.Grid');

      this.X.registerElement('circle', 'foam.graphics.Circle');

      this.Slides.getProperty('installCSS').documentInstallFn.call(
        this.Slides.getPrototype(), this.X);
      this.Slides.getProperty('registerElement').documentInstallFn.call(
        this.Slides.getPrototype(), this.X);

      this.CodeSample.getProperty('installCSS').documentInstallFn.call(
        this.CodeSample.getPrototype(), this.X);
      this.CodeSample.getProperty('registerElement').documentInstallFn.call(
        this.CodeSample.getPrototype(), this.X);

      this.Section.getProperty('installCSS').documentInstallFn.call(
        this.Section.getPrototype(), this.X);
      this.Section.getProperty('registerElement').documentInstallFn.call(
        this.Section.getPrototype(), this.X);
      this.Slides.getProperty('registerElement').documentInstallFn.call(
          this.Slides.getPrototype(), this.X);
    }
  },

  templates: [
    { name: 'toHTML' },
    // (1) Undo font scaling from foam-components styles.
    // (2) Reposition pong component scores within flex container.
    // (3) Code formatting.
    // (4) Two-pane controller styling.
    // (4) Summary list element element styling.
    function CSS() {/*
      slides deck { position: relative; }
      foam-components { font-size: 100%; }
      #pong-container div { display: flex; }
      #pong-container span[name="lScore"] { left: 390px; top:20px; z-index: 2; }
      #pong-container span[name="rScore"] { left: 490px; top:20px; z-index: 2; }
      .pln { color: #000 }
      .str,
      .atv { color: #0f9d58 }
      .kwd,
      .tag { color: #4285f4;  }
      .com { color: #999 }
      .typ,
      .var { color: #673ab7 }
      .lit { color: #db4437 }
      .pun,
      .opn,
      .clo { color: #a3a3a3 }
      .atn { color: #e91e63 }
      .dec,
      .var { color: #e67c73 }
      .fun { color: #fff }
      .twopane-container { height: 100%; width: 100%; }
      slides .twopane-left {
        min-width: 450px;
        max-width: 40%;
        height: 85%;
      }
      slides .twopane-right {
        background: white;
      }
      li { padding-bottom: 2px; }
      li.d1 { margin-left: 16px; font-size: 30px; }
      li.d2 { margin-left: 32px; font-size: 28px; }
      li.d3 { margin-left: 48px; font-size: 26px; }
      li.d4 { margin-left: 64px; font-size: 24px; }
    */}
  ]
});
<|endoftext|>


<commit_before>angular.module('opal.controllers').controller(
    'EpisodeListCtrl', function($scope, $q, $http, $cookieStore,
                                $location, $routeParams,
                                $modal, $rootScope,
                                Flow, Item,
                                Episode, schema, episodes, options,
                                profile,
                                episodeVisibility, viewDischarged){

        var version = window.version;
        // if($cookieStore.get('opal.seenVersion') !=  version){
        //     $('#changelogTooltip').tooltip('show');
        //     $('#changelog').on('hidden.bs.modal', function(){
        //         $cookieStore.put('opal.seenVersion', version)
        //         $('#changelogTooltip').tooltip('hide');
        //     });
        // }

        $scope.state = 'normal';

	    $scope.rix = 0; // row index
	    $scope.cix = 0; // column index
	    $scope.iix = 0; // item index

	    $scope.mouseRix = -1; // index of row mouse is currently over
	    $scope.mouseCix = -1; // index of column mouse is currently over
        $scope._ =  _;

	    $scope.query = {hospital_number: '', name: ''};

        if(viewDischarged){
            $scope.path_base = '/discharge/';
        }else{
            $scope.path_base = '/list/';
        }

        if(!$routeParams.tag){
            var tag =  $cookieStore.get('opal.currentTag') || 'mine';
            $location.path($scope.path_base + tag);
            return
        }
        $scope.currentTag = $routeParams.tag;

        if(!$routeParams.subtag){
            // We now force redirect to the first subtag if there is one
            if($scope.currentTag in options.tag_hierarchy &&
               options.tag_hierarchy[$scope.currentTag].length > 0){
                var subtag = options.tag_hierarchy[$scope.currentTag][0];
                var target = $scope.path_base + $scope.currentTag + '/' + subtag;
                console.log(target);
                $location.path(target);
                return
            }else{
                $scope.currentSubTag = 'all';
            }
        }else{
            $scope.currentSubTag = $routeParams.subtag;
        }
        $scope.columns = schema.columns;

        $scope.profile = profile;
        $scope.tag_display = options.tag_display;

	    function getVisibleEpisodes() {
		    var visibleEpisodes = [];
            var episode_list = [];

            visibleEpisodes = _.filter(episodes, function(episode){
                return episodeVisibility(episode, $scope, viewDischarged)
            });
		    visibleEpisodes.sort(compareEpisodes);
		    return visibleEpisodes;
	    };

	    $scope.rows = getVisibleEpisodes();

	    function compareEpisodes(p1, p2) {
		    return p1.compare(p2);
	    };

        $scope.jumpToTag = function(tag){
            if(_.contains(_.keys(options.tag_hierarchy), tag)){
                $location.path($scope.path_base + tag)
            }else{

                for(var prop in options.tag_hierarchy){
                    if(options.tag_hierarchy.hasOwnProperty(prop)){
                        if(_.contains(_.values(options.tag_hierarchy[prop]), tag)){
                            $location.path($scope.path_base + prop + '/' + tag)
                        }
                    }
                }

            };
        }

        $scope.otherTags = function(episode){
            tags = episode.getTags();
            return _.filter(tags, function(t){
                if(t in options.tag_hierarchy &&
                   options.tag_hierarchy[t].length > 0){ return false };
                if(t == $scope.currentTag){ return false };
                if(t == $scope.currentSubTag){ return false };
                return true
            });
        }

	    $scope.$watch('currentTag', function() {
		    $cookieStore.put('opal.currentTag', $scope.currentTag);
            if($scope.currentTag != $routeParams.tag){
                $scope.state = 'reloading'
            }
            $location.path($scope.path_base +  $scope.currentTag);
	    });

	    $scope.$watch('currentSubTag', function(){
		    $cookieStore.put('opal.currentSubTag', $scope.currentSubTag);
            if($scope.currentSubTag == 'all'){
                if($routeParams.subtag && $scope.currentSubTag != $routeParams.subtag){
                    $scope.state = 'reloading'
                }
                $location.path($scope.path_base +  $scope.currentTag);
            }else{
                if($scope.currentSubTag != $routeParams.subtag){
                    $scope.state = 'reloading'
                }
                $location.path($scope.path_base +  $scope.currentTag + '/' +  $scope.currentSubTag);
            }
	    });

        $scope.showSubtags = function(withsubtags){
            var show =  _.contains(withsubtags, $scope.currentTag);
            return show
        };

	    $scope.$watch('query.hospital_number', function() {
		    $scope.rows = getVisibleEpisodes();
	    });

	    $scope.$watch('query.name', function() {
		    $scope.rows = getVisibleEpisodes();
	    });

	    $scope.$on('keydown', function(event, e) {
		    if ($scope.state == 'normal') {
			    switch (e.keyCode) {
			    case 37: // left
				    goLeft();
				    break;
			    case 39: // right
				    goRight();
				    break;
			    case 38: // up
				    goUp();
				    break;
			    case 40: // down
				    goDown();
				    break;
			    case 13: // enter
			    case 113: // F2
				    $scope.editItem($scope.rix, $scope.cix, $scope.iix);
                    e.preventDefault();
				    break;
			    case 8: // backspace
				    e.preventDefault();
			    case 46: // delete
				    $scope.deleteItem($scope.rix, $scope.cix, $scope.iix);
				    break;
			    case 191: // question mark
				    if(e.shiftKey){
					    showKeyboardShortcuts();
				    }
				    break;
                case 78: // n
                    $scope.addEpisode();
                    e.preventDefault();
                    break;
			    };
		    };
	    });

	    function getColumnName(cix) {
		    return $scope.columns[cix].name;
	    };

	    function getRowIxFromEpisodeId(episodeId) {
		    for (var rix = 0; rix < $scope.rows.length; rix++) {
			    if ($scope.rows[rix].id == episodeId) {
				    return rix;
			    }
		    };
		    return -1;
	    };

	    function getEpisode(rix) {
		    return $scope.rows[rix];
	    };

	    $scope.print = function() {
		    window.print();
	    };

	    $scope.selectItem = function(rix, cix, iix) {
		    $scope.rix = rix;
		    $scope.cix = cix;
		    $scope.iix = iix;
	    };

	    $scope.focusOnQuery = function() {
		    $scope.selectItem(-1, -1, -1);
		    $scope.state = 'search';
	    };

	    $scope.blurOnQuery = function() {
		    if ($scope.rix == -1) {
			    $scope.selectItem(0, 0, 0);
		    };
		    $scope.state = 'normal';
	    };

	    $scope.addEpisode = function() {
            if(profile.readonly){ return null; };

            var enter = Flow(
                'enter', schema, options,
                {
                    current_tags: {
                        tag: $scope.currentTag,
                        subtag: $scope.currentSubTag
                    }
                }
            );

		    $scope.state = 'modal';

            enter.then(
                function(episode) {
		            // User has either retrieved an existing episode or created a new one,
		            // or has cancelled the process at some point.
		            //
		            // This ensures that the relevant episode is added to the table and
		            // selected.
		            var rowIx;
		            $scope.state = 'normal';
		            if (episode && episode != 'cancel') {
			            episodes[episode.id] = episode;
			            $scope.rows = getVisibleEpisodes();
			            rowIx = getRowIxFromEpisodeId(episode.id);
			            $scope.selectItem(rowIx, 0, 0);
		            };
	            },
                function(reason){
                    // The modal has been dismissed. Practically speaking this means
                    // that the Angular UI called dismiss rather than our cancel()
                    // method on the OPAL controller. We just need to re-set in order
                    // to re-enable keybard listeners.
                    $scope.state = 'normal';
                });
	    };

	    $scope.dischargeEpisode = function(rix, event) {
		    var episode = getEpisode(rix);

            if(profile.readonly){ return null; };

		    event.preventDefault();// Required to prevent the page reloading
		    $scope.state = 'modal';

            var exit = Flow(
                'exit', schema, options,
                {
                    current_tags: {
                        tag   : $scope.currentTag,
                        subtag: $scope.currentSubTag
                    },
                    episode: episode
                }
            );

            exit.then(function(result) {
			    $scope.state = 'normal';

			    if (result == 'discharged' | result == 'moved') {
				    $scope.rows = getVisibleEpisodes();
				    $scope.selectItem(0, $scope.cix, 0);
			    };
		    });
	    };

        // TODO: Test This!
        $scope.removeFromMine = function(rix, event){
            if(profile.readonly){
                return null;
            };

            event.preventDefault();

            var modal;
            var episode = getEpisode(rix);
            var location = episode.location[0];
            editing = location.makeCopy();
            // TODO:
            // URGENT: Make this use new tagging
            delete editing.tagging.mine;
            location.save(editing).then(function(result){
                $scope.rows = getVisibleEpisodes();
            })

        };

      _openEditItemModal = function(item, columnName, episode) {
        if(profile.readonly){
          return null;
        };
        $scope.state = 'modal';

        modal = $modal.open({
          templateUrl: '/templates/modals/' + columnName + '.html/',
          controller: 'EditItemCtrl',
          resolve: {
            item: function() { return item; },
            options: function() { return options; },
            episode: function() { return episode; }
          }
        });

        modal.result.then(function(result) {
          $scope.state = 'normal';

          if (columnName == 'tagging') {
            // User may have removed current tag
            $scope.rows = getVisibleEpisodes();
            $scope.selectItem(getRowIxFromEpisodeId(episode.id), $scope.cix, 0);
          }

          if (result == 'save-and-add-another') {
            $scope.editItem(rix, cix, episode.getNumberOfItems(columnName));
          };
                if (item.sort){
                    episode.sortColumn(item.columnName, item.sort);
                };
        }, function(){
                $scope.state = 'normal';
            });
      }

      $scope.newNamedItem = function(episode, name) {
        var item = episode.newItem(name, {column: $rootScope.fields[name]});
        episode[name][0] = item;
        _openEditItemModal(item, name, episode);
      }

      $scope.editNamedItem = function(episode, name, iix) {
        var item;
        if (episode[name][iix].columnName) {
          item = episode[name][iix];
        } else {
          item = new Item(episode[name][iix], episode, $rootScope.fields[name]);
          episode[name][iix] = item;
        }

        _openEditItemModal(item, name, episode);
      }

	    $scope.editItem = function(rix, cix, iix) {
		    var modal;
		    var columnName = getColumnName(cix);
		    var episode = getEpisode(rix);
		    var item;

		    if (iix == episode.getNumberOfItems(columnName)) {
			    item = episode.newItem(columnName, {schema: schema});
		    } else {
			    item = episode.getItem(columnName, iix);
		    };

		    $scope.selectItem(rix, cix, iix);
        _openEditItemModal(item, columnName, episode);
	    };

	    $scope.deleteItem = function(rix, cix, iix) {
		    var modal;
		    var columnName = getColumnName(cix);
		    var episode = getEpisode(rix);
		    var item = episode.getItem(columnName, iix);

            if(profile.readonly){
                return null;
            };

            if (schema.isReadOnly(columnName)) {
                // Cannont delete readonly columns
                return;
            }

		    if (schema.isSingleton(columnName)) {
			    // Cannot delete singleton
			    return;
		    }

		    if (!angular.isDefined(item)) {
			    // Cannot delete 'Add'
			    return;
		    }

		    $scope.state = 'modal'
		    modal = $modal.open({
			    templateUrl: '/templates/modals/delete_item_confirmation.html/',
			    controller: 'DeleteItemConfirmationCtrl',
			    resolve: {
				    item: function() { return item; }
			    }
		    });

		    modal.result.then(function(result) {
			    $scope.state = 'normal';
		    });
	    };

	    $scope.mouseEnter = function(rix, cix) {
		    $scope.mouseRix = rix;
		    $scope.mouseCix = cix;
	    }

	    $scope.mouseLeave = function() {
		    $scope.mouseRix = -1;
		    $scope.mouseCix = -1;
	    }

        function showKeyboardShortcuts(){
		    // TODO fix this
            $('#keyboard-shortcuts').modal();
        };

	    function goLeft() {
		    if ($scope.cix > 0) {
			    $scope.cix--;
			    $scope.iix = 0;
		    };
	    };

	    function goRight() {
		    if ($scope.cix < $scope.columns.length - 1) {
			    $scope.cix++;
			    $scope.iix = 0;
		    };
	    };

	    function goUp() {
		    var episode;
		    var columnName = getColumnName($scope.cix);

		    if ($scope.iix > 0) {
			    $scope.iix--;
		    } else if ($scope.rix > 0) {
			    $scope.rix--;
			    if (schema.isSingleton(columnName)) {
				    $scope.iix = 0;
			    } else {
				    episode = getEpisode($scope.rix);
				    $scope.iix = episode.getNumberOfItems(columnName);
			    };
		    };
	    };

	    function goDown() {
		    var episode = getEpisode($scope.rix);
		    var columnName = getColumnName($scope.cix);

		    if (!schema.isSingleton(columnName) &&
		        ($scope.iix < episode.getNumberOfItems(columnName))) {
			    $scope.iix++;
		    } else if ($scope.rix < $scope.rows.length - 1) {
			    $scope.rix++;
			    $scope.iix = 0;
		    };
	    };
    });
<commit_msg>Initialise episode field if not set<commit_after>angular.module('opal.controllers').controller(
    'EpisodeListCtrl', function($scope, $q, $http, $cookieStore,
                                $location, $routeParams,
                                $modal, $rootScope,
                                Flow, Item,
                                Episode, schema, episodes, options,
                                profile,
                                episodeVisibility, viewDischarged){

        var version = window.version;
        // if($cookieStore.get('opal.seenVersion') !=  version){
        //     $('#changelogTooltip').tooltip('show');
        //     $('#changelog').on('hidden.bs.modal', function(){
        //         $cookieStore.put('opal.seenVersion', version)
        //         $('#changelogTooltip').tooltip('hide');
        //     });
        // }

        $scope.state = 'normal';

	    $scope.rix = 0; // row index
	    $scope.cix = 0; // column index
	    $scope.iix = 0; // item index

	    $scope.mouseRix = -1; // index of row mouse is currently over
	    $scope.mouseCix = -1; // index of column mouse is currently over
        $scope._ =  _;

	    $scope.query = {hospital_number: '', name: ''};

        if(viewDischarged){
            $scope.path_base = '/discharge/';
        }else{
            $scope.path_base = '/list/';
        }

        if(!$routeParams.tag){
            var tag =  $cookieStore.get('opal.currentTag') || 'mine';
            $location.path($scope.path_base + tag);
            return
        }
        $scope.currentTag = $routeParams.tag;

        if(!$routeParams.subtag){
            // We now force redirect to the first subtag if there is one
            if($scope.currentTag in options.tag_hierarchy &&
               options.tag_hierarchy[$scope.currentTag].length > 0){
                var subtag = options.tag_hierarchy[$scope.currentTag][0];
                var target = $scope.path_base + $scope.currentTag + '/' + subtag;
                console.log(target);
                $location.path(target);
                return
            }else{
                $scope.currentSubTag = 'all';
            }
        }else{
            $scope.currentSubTag = $routeParams.subtag;
        }
        $scope.columns = schema.columns;

        $scope.profile = profile;
        $scope.tag_display = options.tag_display;

	    function getVisibleEpisodes() {
		    var visibleEpisodes = [];
            var episode_list = [];

            visibleEpisodes = _.filter(episodes, function(episode){
                return episodeVisibility(episode, $scope, viewDischarged)
            });
		    visibleEpisodes.sort(compareEpisodes);
		    return visibleEpisodes;
	    };

	    $scope.rows = getVisibleEpisodes();

	    function compareEpisodes(p1, p2) {
		    return p1.compare(p2);
	    };

        $scope.jumpToTag = function(tag){
            if(_.contains(_.keys(options.tag_hierarchy), tag)){
                $location.path($scope.path_base + tag)
            }else{

                for(var prop in options.tag_hierarchy){
                    if(options.tag_hierarchy.hasOwnProperty(prop)){
                        if(_.contains(_.values(options.tag_hierarchy[prop]), tag)){
                            $location.path($scope.path_base + prop + '/' + tag)
                        }
                    }
                }

            };
        }

        $scope.otherTags = function(episode){
            tags = episode.getTags();
            return _.filter(tags, function(t){
                if(t in options.tag_hierarchy &&
                   options.tag_hierarchy[t].length > 0){ return false };
                if(t == $scope.currentTag){ return false };
                if(t == $scope.currentSubTag){ return false };
                return true
            });
        }

	    $scope.$watch('currentTag', function() {
		    $cookieStore.put('opal.currentTag', $scope.currentTag);
            if($scope.currentTag != $routeParams.tag){
                $scope.state = 'reloading'
            }
            $location.path($scope.path_base +  $scope.currentTag);
	    });

	    $scope.$watch('currentSubTag', function(){
		    $cookieStore.put('opal.currentSubTag', $scope.currentSubTag);
            if($scope.currentSubTag == 'all'){
                if($routeParams.subtag && $scope.currentSubTag != $routeParams.subtag){
                    $scope.state = 'reloading'
                }
                $location.path($scope.path_base +  $scope.currentTag);
            }else{
                if($scope.currentSubTag != $routeParams.subtag){
                    $scope.state = 'reloading'
                }
                $location.path($scope.path_base +  $scope.currentTag + '/' +  $scope.currentSubTag);
            }
	    });

        $scope.showSubtags = function(withsubtags){
            var show =  _.contains(withsubtags, $scope.currentTag);
            return show
        };

	    $scope.$watch('query.hospital_number', function() {
		    $scope.rows = getVisibleEpisodes();
	    });

	    $scope.$watch('query.name', function() {
		    $scope.rows = getVisibleEpisodes();
	    });

	    $scope.$on('keydown', function(event, e) {
		    if ($scope.state == 'normal') {
			    switch (e.keyCode) {
			    case 37: // left
				    goLeft();
				    break;
			    case 39: // right
				    goRight();
				    break;
			    case 38: // up
				    goUp();
				    break;
			    case 40: // down
				    goDown();
				    break;
			    case 13: // enter
			    case 113: // F2
				    $scope.editItem($scope.rix, $scope.cix, $scope.iix);
                    e.preventDefault();
				    break;
			    case 8: // backspace
				    e.preventDefault();
			    case 46: // delete
				    $scope.deleteItem($scope.rix, $scope.cix, $scope.iix);
				    break;
			    case 191: // question mark
				    if(e.shiftKey){
					    showKeyboardShortcuts();
				    }
				    break;
                case 78: // n
                    $scope.addEpisode();
                    e.preventDefault();
                    break;
			    };
		    };
	    });

	    function getColumnName(cix) {
		    return $scope.columns[cix].name;
	    };

	    function getRowIxFromEpisodeId(episodeId) {
		    for (var rix = 0; rix < $scope.rows.length; rix++) {
			    if ($scope.rows[rix].id == episodeId) {
				    return rix;
			    }
		    };
		    return -1;
	    };

	    function getEpisode(rix) {
		    return $scope.rows[rix];
	    };

	    $scope.print = function() {
		    window.print();
	    };

	    $scope.selectItem = function(rix, cix, iix) {
		    $scope.rix = rix;
		    $scope.cix = cix;
		    $scope.iix = iix;
	    };

	    $scope.focusOnQuery = function() {
		    $scope.selectItem(-1, -1, -1);
		    $scope.state = 'search';
	    };

	    $scope.blurOnQuery = function() {
		    if ($scope.rix == -1) {
			    $scope.selectItem(0, 0, 0);
		    };
		    $scope.state = 'normal';
	    };

	    $scope.addEpisode = function() {
            if(profile.readonly){ return null; };

            var enter = Flow(
                'enter', schema, options,
                {
                    current_tags: {
                        tag: $scope.currentTag,
                        subtag: $scope.currentSubTag
                    }
                }
            );

		    $scope.state = 'modal';

            enter.then(
                function(episode) {
		            // User has either retrieved an existing episode or created a new one,
		            // or has cancelled the process at some point.
		            //
		            // This ensures that the relevant episode is added to the table and
		            // selected.
		            var rowIx;
		            $scope.state = 'normal';
		            if (episode && episode != 'cancel') {
			            episodes[episode.id] = episode;
			            $scope.rows = getVisibleEpisodes();
			            rowIx = getRowIxFromEpisodeId(episode.id);
			            $scope.selectItem(rowIx, 0, 0);
		            };
	            },
                function(reason){
                    // The modal has been dismissed. Practically speaking this means
                    // that the Angular UI called dismiss rather than our cancel()
                    // method on the OPAL controller. We just need to re-set in order
                    // to re-enable keybard listeners.
                    $scope.state = 'normal';
                });
	    };

	    $scope.dischargeEpisode = function(rix, event) {
		    var episode = getEpisode(rix);

            if(profile.readonly){ return null; };

		    event.preventDefault();// Required to prevent the page reloading
		    $scope.state = 'modal';

            var exit = Flow(
                'exit', schema, options,
                {
                    current_tags: {
                        tag   : $scope.currentTag,
                        subtag: $scope.currentSubTag
                    },
                    episode: episode
                }
            );

            exit.then(function(result) {
			    $scope.state = 'normal';

			    if (result == 'discharged' | result == 'moved') {
				    $scope.rows = getVisibleEpisodes();
				    $scope.selectItem(0, $scope.cix, 0);
			    };
		    });
	    };

        // TODO: Test This!
        $scope.removeFromMine = function(rix, event){
            if(profile.readonly){
                return null;
            };

            event.preventDefault();

            var modal;
            var episode = getEpisode(rix);
            var location = episode.location[0];
            editing = location.makeCopy();
            // TODO:
            // URGENT: Make this use new tagging
            delete editing.tagging.mine;
            location.save(editing).then(function(result){
                $scope.rows = getVisibleEpisodes();
            })

        };

      _openEditItemModal = function(item, columnName, episode) {
        if(profile.readonly){
          return null;
        };
        $scope.state = 'modal';

        modal = $modal.open({
          templateUrl: '/templates/modals/' + columnName + '.html/',
          controller: 'EditItemCtrl',
          resolve: {
            item: function() { return item; },
            options: function() { return options; },
            episode: function() { return episode; }
          }
        });

        modal.result.then(function(result) {
          $scope.state = 'normal';

          if (columnName == 'tagging') {
            // User may have removed current tag
            $scope.rows = getVisibleEpisodes();
            $scope.selectItem(getRowIxFromEpisodeId(episode.id), $scope.cix, 0);
          }

          if (result == 'save-and-add-another') {
            $scope.editItem(rix, cix, episode.getNumberOfItems(columnName));
          };
                if (item.sort){
                    episode.sortColumn(item.columnName, item.sort);
                };
        }, function(){
                $scope.state = 'normal';
            });
      }

      $scope.newNamedItem = function(episode, name) {
        var item = episode.newItem(name, {column: $rootScope.fields[name]});
        if (!episode[name]) {
          episode[name] = [];
        }
        episode[name][0] = item;
        _openEditItemModal(item, name, episode);
      }

      $scope.editNamedItem = function(episode, name, iix) {
        var item;
        if (episode[name][iix].columnName) {
          item = episode[name][iix];
        } else {
          item = new Item(episode[name][iix], episode, $rootScope.fields[name]);
          episode[name][iix] = item;
        }

        _openEditItemModal(item, name, episode);
      }

	    $scope.editItem = function(rix, cix, iix) {
		    var modal;
		    var columnName = getColumnName(cix);
		    var episode = getEpisode(rix);
		    var item;

		    if (iix == episode.getNumberOfItems(columnName)) {
			    item = episode.newItem(columnName, {schema: schema});
		    } else {
			    item = episode.getItem(columnName, iix);
		    };

		    $scope.selectItem(rix, cix, iix);
        _openEditItemModal(item, columnName, episode);
	    };

	    $scope.deleteItem = function(rix, cix, iix) {
		    var modal;
		    var columnName = getColumnName(cix);
		    var episode = getEpisode(rix);
		    var item = episode.getItem(columnName, iix);

            if(profile.readonly){
                return null;
            };

            if (schema.isReadOnly(columnName)) {
                // Cannont delete readonly columns
                return;
            }

		    if (schema.isSingleton(columnName)) {
			    // Cannot delete singleton
			    return;
		    }

		    if (!angular.isDefined(item)) {
			    // Cannot delete 'Add'
			    return;
		    }

		    $scope.state = 'modal'
		    modal = $modal.open({
			    templateUrl: '/templates/modals/delete_item_confirmation.html/',
			    controller: 'DeleteItemConfirmationCtrl',
			    resolve: {
				    item: function() { return item; }
			    }
		    });

		    modal.result.then(function(result) {
			    $scope.state = 'normal';
		    });
	    };

	    $scope.mouseEnter = function(rix, cix) {
		    $scope.mouseRix = rix;
		    $scope.mouseCix = cix;
	    }

	    $scope.mouseLeave = function() {
		    $scope.mouseRix = -1;
		    $scope.mouseCix = -1;
	    }

        function showKeyboardShortcuts(){
		    // TODO fix this
            $('#keyboard-shortcuts').modal();
        };

	    function goLeft() {
		    if ($scope.cix > 0) {
			    $scope.cix--;
			    $scope.iix = 0;
		    };
	    };

	    function goRight() {
		    if ($scope.cix < $scope.columns.length - 1) {
			    $scope.cix++;
			    $scope.iix = 0;
		    };
	    };

	    function goUp() {
		    var episode;
		    var columnName = getColumnName($scope.cix);

		    if ($scope.iix > 0) {
			    $scope.iix--;
		    } else if ($scope.rix > 0) {
			    $scope.rix--;
			    if (schema.isSingleton(columnName)) {
				    $scope.iix = 0;
			    } else {
				    episode = getEpisode($scope.rix);
				    $scope.iix = episode.getNumberOfItems(columnName);
			    };
		    };
	    };

	    function goDown() {
		    var episode = getEpisode($scope.rix);
		    var columnName = getColumnName($scope.cix);

		    if (!schema.isSingleton(columnName) &&
		        ($scope.iix < episode.getNumberOfItems(columnName))) {
			    $scope.iix++;
		    } else if ($scope.rix < $scope.rows.length - 1) {
			    $scope.rix++;
			    $scope.iix = 0;
		    };
	    };
    });
<|endoftext|>


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

CLASS({
  name: 'BasePropertyView',
  package: 'foam.ui',
  extendsModel: 'foam.ui.BaseView',

  requires: ['foam.ui.TextFieldView'],

  documentation: function() {/*
    Apply this trait to a $$DOC{ref:'BaseView'} (such as $$DOC{ref:'HTMLView'}).

Used by $$DOC{ref:'foam.ui.DetailView'} to generate a sub-$$DOC{ref:'foam.ui.View'} for one
    $$DOC{ref:'Property'}. The $$DOC{ref:'foam.ui.View'} chosen can be based off the
    $$DOC{ref:'Property.view',text:'Property.view'} value, the $$DOC{ref:'.innerView'} value, or
    $$DOC{ref:'.args'}.model_.
  */},

  properties: [
    {
      name: 'prop',
      type: 'Property',
      documentation: function() {/*
          The $$DOC{ref:'Property'} for which to generate a $$DOC{ref:'foam.ui.View'}.
      */},
      postSet: function(old, nu) {
        if ( old && this.bound_ ) this.unbindData(this.data);
        if ( nu && ! this.bound_ ) this.bindData(this.data);
      }
    },
    {
      name: 'data',
      documentation: function() {/*
          The $$DOC{ref:'.data'} for which to generate a $$DOC{ref:'foam.ui.View'}.
      */},
      postSet: function(old, nu) {
        if ( old && this.bound_ ) this.unbindData(old);
        if ( nu ) this.bindData(nu);
      }
    },
    {
      name: 'childData'
    },
    {
      name: 'parent',
      type: 'foam.ui.View',
      postSet: function(_, p) {
        if (!p) return; // TODO(jacksonic): We shouldn't pretend we aren't part of the tree
        p[this.prop.name + 'View'] = this.view;
        if ( this.view ) this.view.parent = p;
      },
      documentation: function() {/*
        The $$DOC{ref:'foam.ui.View'} to use as the parent container for the new
        sub-$$DOC{ref:'foam.ui.View'}.
      */}
    },
    {
      name: 'innerView',
      help: 'Override for prop.view',
      documentation: function() {/*
        The optional name of the desired sub-$$DOC{ref:'foam.ui.View'}. If not specified,
        prop.$$DOC{ref:'Property.view'} is used.
      */}
    },
    {
      name: 'view',
      type: 'foam.ui.View',
      documentation: function() {/*
        The new sub-$$DOC{ref:'foam.ui.View'} generated for the given $$DOC{ref:'Property'}.
      */}
    },
    {
      name: 'args',
      documentation: function() {/*
        Optional arguments to be used for sub-$$DOC{ref:'foam.ui.View'} creation. args.model_
        in particular specifies the exact $$DOC{ref:'foam.ui.View'} to use.
      */}
    },
    {
      name: 'bound_',
      model_: 'BooleanProperty',
      defaultValue: false
    }
  ],

  methods: {
    init: function() {
      this.SUPER();
      this.construct();
    },

    fromElement: function(e) {
      this.view.fromElement(e);
      return this;
    },

    createViewFromProperty: function(prop, ret) {
      /* Helper to determine the $$DOC{ref:'foam.ui.View'} to use. */
      var viewName = this.innerView || prop.view
      if ( ! viewName ) ret(this.TextFieldView.create(prop, this.Y));
      else if ( typeof viewName === 'string' ) {
//        var m = this.Y.lookup(viewName);
//        if ( m ) ret(m.create(prop, this.Y));
//        else
          arequire(viewName, this.X)(function(m) { ret(m.create(prop, this.Y)); }.bind(this) );
      }
      else if ( viewName.model_ && typeof viewName.model_ === 'string' ) {
        var m = FOAM(prop.view);
        arequireModel(m, this.X)(ret);
      }
      else if ( viewName.model_ ) {
        var v = viewName.model_.create(viewName, this.X);
        var vId = v.id;
        v.copyFrom(prop);
        v.id = vId;
        ret(v);
      }
      else if ( viewName.factory_ ) {
        var v = this.X.lookup(viewName.factory_).create(viewName, this.X);
        var vId = v.id;
        v.copyFrom(prop);
        v.id = vId;
        ret(v);
      }
      else if ( typeof viewName === 'function' ) ret(viewName(prop, this));

      else ret(viewName.create(prop));
    },

    unbindData: function(oldData) {
      if ( ! this.bound_ || ! oldData || ! this.prop ) return;
      var pValue = oldData.propertyValue(this.prop.name);
      Events.unlink(pValue, this.childData$);
      this.bound_ = false;
    },

    bindData: function(data) {
      if ( this.bound_ || ! data || ! this.prop) return;
      var pValue = data.propertyValue(this.prop.name);
      Events.link(pValue, this.childData$);
      this.bound_ = true;
    },

    toString: function() { /* Name info. */ return 'PropertyView(' + this.prop.name + ', ' + this.view + ')'; },

    destroy: function( isParentDestroyed ) { /* Passthrough to $$DOC{ref:'.view'} */
      // always unbind, since if our parent was the top level destroy we need
      // to unhook if we were created as an addSelfDataChild
      this.unbindData(this.data);
      this.SUPER( isParentDestroyed );
    },

    finishPropertyRender: function() { /* Implement to render your property. */ },

    construct: function() {
      // if not bound yet and we do have data set, bind it
      this.bindData(this.data);

      if ( this.args && this.args.model_ ) {
        var model = this.Y.lookup(this.args.model_);
        console.assert( model, 'Unknown View: ' + this.args.model_);
        // HACK to make sure model specification makes it into the create
        if ( this.args.model ) this.prop.model = this.args.model;
        var view = model.create(this.prop, this.Y);
        delete this.args.model_;
        // syncronous case
        this.finishConstruct(view, this.finishPropertyRender.bind(this));
      } else {
        view = this.createViewFromProperty(this.prop,
                 function(v) {
                   this.finishConstruct(v, this.finishPropertyRender.bind(this));
                 }.bind(this));
      }
    },

    finishConstruct: function(view, ret) {
      view.copyFrom(this.args);
      view.parent = this.parent;
      view.prop = this.prop;

      // TODO(kgr): re-enable when improved
      // if ( this.prop.description || this.prop.help ) view.tooltip = this.prop.description || this.prop.help;

      this.view = view;
      this.addDataChild(view);

      ret && ret();
    },

    addDataChild: function(child) {
      Events.link(this.childData$, child.data$);
      this.addChild(child);
    }
  },

});

<commit_msg>Code cleanup.<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.ui',
  name: 'BasePropertyView',
  extendsModel: 'foam.ui.BaseView',

  requires: [ 'foam.ui.TextFieldView' ],

  documentation: function() {/*
    Apply this trait to a $$DOC{ref:'BaseView'} (such as $$DOC{ref:'HTMLView'}).

Used by $$DOC{ref:'foam.ui.DetailView'} to generate a sub-$$DOC{ref:'foam.ui.View'} for one
    $$DOC{ref:'Property'}. The $$DOC{ref:'foam.ui.View'} chosen can be based off the
    $$DOC{ref:'Property.view',text:'Property.view'} value, the $$DOC{ref:'.innerView'} value, or
    $$DOC{ref:'.args'}.model_.
  */},

  properties: [
    {
      name: 'prop',
      type: 'Property',
      documentation: function() {/*
          The $$DOC{ref:'Property'} for which to generate a $$DOC{ref:'foam.ui.View'}.
      */},
      postSet: function(old, nu) {
        if ( old && this.bound_ ) this.unbindData(this.data);
        if ( nu && ! this.bound_ ) this.bindData(this.data);
      }
    },
    {
      name: 'data',
      documentation: function() {/*
          The $$DOC{ref:'.data'} for which to generate a $$DOC{ref:'foam.ui.View'}.
      */},
      postSet: function(old, nu) {
        if ( old && this.bound_ ) this.unbindData(old);
        if ( nu ) this.bindData(nu);
      }
    },
    {
      name: 'childData'
    },
    {
      name: 'parent',
      type: 'foam.ui.View',
      postSet: function(_, p) {
        if (!p) return; // TODO(jacksonic): We shouldn't pretend we aren't part of the tree
        p[this.prop.name + 'View'] = this.view;
        if ( this.view ) this.view.parent = p;
      },
      documentation: function() {/*
        The $$DOC{ref:'foam.ui.View'} to use as the parent container for the new
        sub-$$DOC{ref:'foam.ui.View'}.
      */}
    },
    {
      name: 'innerView',
      help: 'Override for prop.view',
      documentation: function() {/*
        The optional name of the desired sub-$$DOC{ref:'foam.ui.View'}. If not specified,
        prop.$$DOC{ref:'Property.view'} is used.
      */}
    },
    {
      name: 'view',
      type: 'foam.ui.View',
      documentation: function() {/*
        The new sub-$$DOC{ref:'foam.ui.View'} generated for the given $$DOC{ref:'Property'}.
      */}
    },
    {
      name: 'args',
      documentation: function() {/*
        Optional arguments to be used for sub-$$DOC{ref:'foam.ui.View'} creation. args.model_
        in particular specifies the exact $$DOC{ref:'foam.ui.View'} to use.
      */}
    },
    {
      name: 'bound_',
      model_: 'BooleanProperty',
      defaultValue: false
    }
  ],

  methods: {
    init: function() {
      this.SUPER();
      this.construct();
    },

    fromElement: function(e) {
      this.view.fromElement(e);
      return this;
    },

    createViewFromProperty: function(prop, ret) {
      /* Helper to determine the $$DOC{ref:'foam.ui.View'} to use. */
      var viewName = this.innerView || prop.view
      if ( ! viewName ) ret(this.TextFieldView.create(prop, this.Y));
      else if ( typeof viewName === 'string' ) {
//        var m = this.Y.lookup(viewName);
//        if ( m ) ret(m.create(prop, this.Y));
//        else
          arequire(viewName, this.X)(function(m) { ret(m.create(prop, this.Y)); }.bind(this) );
      }
      else if ( viewName.model_ && typeof viewName.model_ === 'string' ) {
        var m = FOAM(prop.view);
        arequireModel(m, this.X)(ret);
      }
      else if ( viewName.model_ ) {
        var v = viewName.model_.create(viewName, this.X);
        var vId = v.id;
        v.copyFrom(prop);
        v.id = vId;
        ret(v);
      }
      else if ( viewName.factory_ ) {
        var v = this.X.lookup(viewName.factory_).create(viewName, this.X);
        var vId = v.id;
        v.copyFrom(prop);
        v.id = vId;
        ret(v);
      }
      else if ( typeof viewName === 'function' ) ret(viewName(prop, this));

      else ret(viewName.create(prop));
    },

    unbindData: function(oldData) {
      if ( ! this.bound_ || ! oldData || ! this.prop ) return;
      var pValue = oldData.propertyValue(this.prop.name);
      Events.unlink(pValue, this.childData$);
      this.bound_ = false;
    },

    bindData: function(data) {
      if ( this.bound_ || ! data || ! this.prop) return;
      var pValue = data.propertyValue(this.prop.name);
      Events.link(pValue, this.childData$);
      this.bound_ = true;
    },

    toString: function() { /* Name info. */ return 'PropertyView(' + this.prop.name + ', ' + this.view + ')'; },

    destroy: function( isParentDestroyed ) { /* Passthrough to $$DOC{ref:'.view'} */
      // always unbind, since if our parent was the top level destroy we need
      // to unhook if we were created as an addSelfDataChild
      this.unbindData(this.data);
      this.SUPER( isParentDestroyed );
    },

    finishPropertyRender: function() { /* Implement to render your property. */ },

    construct: function() {
      // if not bound yet and we do have data set, bind it
      this.bindData(this.data);

      if ( this.args && this.args.model_ ) {
        var model = this.Y.lookup(this.args.model_);
        console.assert( model, 'Unknown View: ' + this.args.model_);
        // HACK to make sure model specification makes it into the create
        if ( this.args.model ) this.prop.model = this.args.model;
        var view = model.create(this.prop, this.Y);
        delete this.args.model_;
        // syncronous case
        this.finishConstruct(view, this.finishPropertyRender.bind(this));
      } else {
        view = this.createViewFromProperty(this.prop,
                 function(v) {
                   this.finishConstruct(v, this.finishPropertyRender.bind(this));
                 }.bind(this));
      }
    },

    finishConstruct: function(view, ret) {
      view.copyFrom(this.args);
      view.parent = this.parent;
      view.prop = this.prop;

      // TODO(kgr): re-enable when improved
      // if ( this.prop.description || this.prop.help ) view.tooltip = this.prop.description || this.prop.help;

      this.view = view;
      this.addDataChild(view);

      ret && ret();
    },

    addDataChild: function(child) {
      Events.link(this.childData$, child.data$);
      this.addChild(child);
    }
  },

});
<|endoftext|>


<commit_before>/*
 * bValidator bs3popover theme
 */

bValidator.defaultOptions.themes.bs3tooltip = {

    tooltipOptions : { // see: http://getbootstrap.com/javascript/#tooltips-options
        html     : 'true',
        viewport : null,
        trigger  : 'manual'
    },
    placement : 'right', // top | bottom | left | right
    content   : '
{message}
',
    contentClose : '
{message}

<button type="button" class="close" aria-label="Close">×</button>
',
    showClose : true,

    dataOptionNamespace : 'bvalidatorTheme', // data-bvalidator-theme- attributes
    presenter : 'Bs3PopoverTooltipPresenter' // or constructor function
}

// set as default theme
bValidator.defaultOptions.useTheme = 'bs3tooltip';
<commit_msg>added sanitize:false option for tooltip to work with bootstrap v3.4.1<commit_after>/*
 * bValidator bs3popover theme
 */

bValidator.defaultOptions.themes.bs3tooltip = {

    tooltipOptions : {
        html     : true,
        viewport : null,
        trigger  : 'manual',
        sanitize : false
    },
    placement : 'right', // top | bottom | left | right
    content   : '
{message}
',
    contentClose : '
{message}

<button type="button" class="close" aria-label="Close">×</button>
',
    showClose : true,

    dataOptionNamespace : 'bvalidatorTheme', // data-bvalidator-theme- attributes
    presenter : 'Bs3PopoverTooltipPresenter' // or constructor function
}

// set as default theme
bValidator.defaultOptions.useTheme = 'bs3tooltip';
<|endoftext|>