<commit_before>// Copyright 2002-2013, University of Colorado

/*
 * A bucket that can be used to store spherical objects.  Manages the addition
 * and removal of the spheres, and stacks them as the are added, and manages
 * the stack as spheres are removed.
 *
 * This expects the spheres to have certain properties, please inspect the
 * code to understand the "contract" between the bucket and the spheres.
 *
 * @author John Blanco
 */
define( function ( require ) {

  var Vector2 = require( 'DOT/Vector2' );

  function SphereBucket( options ) {
    this.position = options.position || Vector2.ZERO;
    this.color = options.color || 'orange';
    this.width = options.width || 100;
    this.particleRadius = options.particleRadius || 10;
    this.yOffset = this.particleRadius; // Empirically determined, for positioning particles inside the bucket.
    this.labelText = options.labelText;
    this.particles = [];
  }

  SphereBucket.prototype.addParticleFirstOpen = function ( particle ) {
    particle.position = this.getFirstOpenLocation();
    this.particles.push( particle );
    var thisBucket = this;
    particle.link( 'userControlled', function ( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } )
  }

  SphereBucket.prototype.addParticleNearestOpen = function ( particle ) {
    particle.position = this.getNearestOpenLocation( particle.position );
    this.particles.push( particle );
    var thisBucket = this;
    particle.link( 'userControlled', function ( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } )
  }

  SphereBucket.prototype.removeParticle = function ( particle ) {
    if ( this.particles.indexOf( particle ) == -1 ) {
      console.log( "Error: Attempt to remove particle not contained in bucket, ignoring." );
      return;
    }
    this.particles = _.without( this.particles, particle );
    this.relayoutBucketParticles();
  }

  SphereBucket.prototype.isPositionOpen = function ( position ) {
    var positionOpen = true;
    for ( var i = 0; i < this.particles.length; i++ ) {
      var particle = this.particles[ i ];
      if ( particle.position.equals( position ) ) {
        positionOpen = false;
        break;
      }
    }
    return positionOpen;
  }

  SphereBucket.prototype.getFirstOpenLocation = function () {
    var openLocation = Vector2.ZERO;
    var usableWidth = this.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = this.particleRadius * 2;
    var numParticlesInLayer = Math.floor( usableWidth / ( this.particleRadius * 2 ) );
    var row = 0;
    var positionInLayer = 0;
    var found = false;
    while ( !found ) {
      var testLocation = new Vector2( this.position.x - this.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                      this.getYPositionForLayer( row ) );
      if ( this.isPositionOpen( testLocation ) ) {
        // We found a location that is open.
        openLocation = testLocation;
        found = true;
        continue;
      }
      else {
        positionInLayer++;
        if ( positionInLayer >= numParticlesInLayer ) {
          // Move to the next layer.
          row++;
          positionInLayer = 0;
          numParticlesInLayer--;
          offsetFromBucketEdge += this.particleRadius;
          if ( numParticlesInLayer == 0 ) {
            // This algorithm doesn't handle the situation where
            // more particles are added than can be stacked into
            // a pyramid of the needed size, but so far it hasn't
            // needed to.  If this requirement changes, the
            // algorithm will need to change too.
            numParticlesInLayer = 1;
            offsetFromBucketEdge -= this.particleRadius;
          }
        }
      }
    }
    return openLocation;
  }

  SphereBucket.prototype.isPositionOpen = function ( position ) {
    var positionOpen = true;
    _.each( this.particles, function ( particle ) {
      if ( particle.position.equals( position ) ) {
        positionOpen = false;
      }
    } );
    return positionOpen;
  }

  SphereBucket.prototype.getLayerForYPosition = function ( yPosition ) {
    return Math.abs( Math.round( ( yPosition - ( this.y + this.yOffset ) ) / ( this.particleRadius * 2 * 0.866 ) ) );
  }

  /*
   * Get the nearest open location to the provided current location.  This
   * is used for particle stacking.
   *
   * @param location
   * @return
   */
  SphereBucket.prototype.getNearestOpenLocation = function ( position ) {
    // Determine the highest occupied layer.  The bottom layer is 0.
    var highestOccupiedLayer = 0;
    var self = this;
    _.each( this.particles, function ( particle ) {
      var layer = self.getLayerForYPosition( particle.position.y );
      if ( layer > highestOccupiedLayer ) {
        highestOccupiedLayer = layer;
      }
    } );

    // Make a list of all open locations in the occupied layers.
    var openLocations = [];
    var placeableWidth = this.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = ( this.width - placeableWidth ) / 2 + this.particleRadius;
    var numParticlesInLayer = Math.floor( placeableWidth / ( this.particleRadius * 2 ) );

    // Loop, searching for open positions in the particle stack.
    for ( var layer = 0; layer <= highestOccupiedLayer + 1; layer++ ) {

      // Add all open locations in the current layer.
      for ( var positionInLayer = 0; positionInLayer < numParticlesInLayer; positionInLayer++ ) {
        var testPosition = new Vector2( this.x - this.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                        this.getYPositionForLayer( layer ) );
        if ( this.isPositionOpen( testPosition ) ) {

          // We found a location that is unoccupied.
          if ( layer == 0 || this.countSupportingParticles( testPosition ) == 2 ) {
            // This is a valid open location.
            openLocations.push( testPosition );
          }
        }
      }

      // Adjust variables for the next layer.
      numParticlesInLayer--;
      offsetFromBucketEdge += this.particleRadius;
      if ( numParticlesInLayer == 0 ) {
        // If the stacking pyramid is full, meaning that there are
        // no locations that are open within it, this algorithm
        // classifies the locations directly above the top of the
        // pyramid as being open.  This would result in a stack
        // of particles with a pyramid base.  So far, this hasn't
        // been a problem, but this limitation may limit
        // reusability of this algorithm.
        numParticlesInLayer = 1;
        offsetFromBucketEdge -= this.particleRadius;
      }
    }

    // Find the closest open location to the provided current location.
    // Only the X-component is used for this determination, because if
    // the Y-component is used the particles often appear to fall sideways
    // when released above the bucket, which just looks weird.
    var closestOpenLocation = openLocations[0] || Vector2.ZERO;

    _.each( openLocations, function ( location ) {
      if ( location.distance( position ) < closestOpenLocation.distance( position )){
        // This location is closer.
        closestOpenLocation = location;
      }
    } )
    return closestOpenLocation;
  }

  SphereBucket.prototype.getYPositionForLayer = function ( layer ) {
    return this.y + this.yOffset - layer * this.particleRadius * 2 * 0.866;
  }

  /*
   * Determine whether a particle is 'dangling', i.e. hanging above an open
   * space in the stack of particles.  Dangling particles should fall.
   */
  SphereBucket.prototype.isDangling = function ( particle ) {
    var onBottomRow = particle.position.y === this.position.y + this.yOffset;
    return !onBottomRow && this.countSupportingParticles( particle.position.x, particle.position.y ) < 2;
  }

  SphereBucket.prototype.countSupportingParticles = function ( position ) {
    var count = 0;
    for ( var i = 0; i < this.particles.length; i++ ) {
      p = this.particles[i];
      if ( p.position.y > position.y && //must be in a lower layer (and larger y is further down on the page).
           p.position.distance( position ) < this.particleRadius * 3 ) {
        // Must be a supporting particle.
        count++;
      }
    }
    return count;
  }

  SphereBucket.prototype.relayoutBucketParticles = function () {
    do {
      for ( var i = 0; i < this.particles.length; i++ ) {
        var particleMoved = false;
        var particle = this.particles[i];
        if ( this.isDangling( particle ) ) {
          particle.position = this.getNearestOpenLocation( particle.position );
          particleMoved = true;
          break;
        }
      }
    } while ( particleMoved );
  }

  return SphereBucket;
} );
<commit_msg>Made several fixes to the particle stacking algorithm, made this class extend "Bucket".<commit_after>// Copyright 2002-2013, University of Colorado

/*
 * A bucket that can be used to store spherical objects.  Manages the addition
 * and removal of the spheres, and stacks them as the are added, and manages
 * the stack as spheres are removed.
 *
 * This expects the spheres to have certain properties, please inspect the
 * code to understand the "contract" between the bucket and the spheres.
 *
 * @author John Blanco
 */
define( function ( require ) {

  var Vector2 = require( 'DOT/Vector2' );
  var Inheritance = require( 'PHETCOMMON/util/Inheritance' );
  var Bucket = require( 'PHETCOMMON/model/Bucket' );

  function SphereBucket( options ) {
    Bucket.call( this, options );
    this.particleRadius = options.particleRadius || 10;
    this.yOffset = this.particleRadius; // Empirically determined, for positioning particles inside the bucket.
    this.particles = [];
  }

  // Inherit from base type.
  Inheritance.inheritPrototype( SphereBucket, Bucket );

  SphereBucket.prototype.addParticleFirstOpen = function ( particle ) {
    particle.position = this.getFirstOpenLocation();
    this.particles.push( particle );
    var thisBucket = this;
    particle.link( 'userControlled', function ( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } )
  }

  SphereBucket.prototype.addParticleNearestOpen = function ( particle ) {
    particle.position = this.getNearestOpenLocation( particle.position );
    this.particles.push( particle );
    var thisBucket = this;
    particle.link( 'userControlled', function ( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } )
  }

  SphereBucket.prototype.removeParticle = function ( particle ) {
    if ( this.particles.indexOf( particle ) == -1 ) {
      console.log( "Error: Attempt to remove particle not contained in bucket, ignoring." );
      return;
    }
    this.particles = _.without( this.particles, particle );
    this.relayoutBucketParticles();
  }

  SphereBucket.prototype.isPositionOpen = function ( position ) {
    var positionOpen = true;
    for ( var i = 0; i < this.particles.length; i++ ) {
      var particle = this.particles[ i ];
      if ( particle.position.equals( position ) ) {
        positionOpen = false;
        break;
      }
    }
    return positionOpen;
  }

  SphereBucket.prototype.getFirstOpenLocation = function () {
    var openLocation = Vector2.ZERO;
    var usableWidth = this.size.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = this.particleRadius * 2;
    var numParticlesInLayer = Math.floor( usableWidth / ( this.particleRadius * 2 ) );
    var row = 0;
    var positionInLayer = 0;
    var found = false;
    debugger;
    while ( !found ) {
      var testLocation = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                      this.getYPositionForLayer( row ) );
      if ( this.isPositionOpen( testLocation ) ) {
        // We found a location that is open.
        openLocation = testLocation;
        found = true;
        continue;
      }
      else {
        positionInLayer++;
        if ( positionInLayer >= numParticlesInLayer ) {
          // Move to the next layer.
          row++;
          positionInLayer = 0;
          numParticlesInLayer--;
          offsetFromBucketEdge += this.particleRadius;
          if ( numParticlesInLayer == 0 ) {
            // This algorithm doesn't handle the situation where
            // more particles are added than can be stacked into
            // a pyramid of the needed size, but so far it hasn't
            // needed to.  If this requirement changes, the
            // algorithm will need to change too.
            numParticlesInLayer = 1;
            offsetFromBucketEdge -= this.particleRadius;
          }
        }
      }
    }
    return openLocation;
  }

  SphereBucket.prototype.isPositionOpen = function ( position ) {
    var positionOpen = true;
    _.each( this.particles, function ( particle ) {
      if ( particle.position.equals( position ) ) {
        positionOpen = false;
      }
    } );
    return positionOpen;
  }

  SphereBucket.prototype.getLayerForYPosition = function ( yPosition ) {
    return Math.abs( Math.round( ( yPosition - ( this.y + this.yOffset ) ) / ( this.particleRadius * 2 * 0.866 ) ) );
  }

  /*
   * Get the nearest open location to the provided current location.  This
   * is used for particle stacking.
   *
   * @param location
   * @return
   */
  SphereBucket.prototype.getNearestOpenLocation = function ( position ) {
    // Determine the highest occupied layer.  The bottom layer is 0.
    var highestOccupiedLayer = 0;
    var self = this;
    _.each( this.particles, function ( particle ) {
      var layer = self.getLayerForYPosition( particle.position.y );
      if ( layer > highestOccupiedLayer ) {
        highestOccupiedLayer = layer;
      }
    } );

    // Make a list of all open locations in the occupied layers.
    var openLocations = [];
    var placeableWidth = this.size.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = ( this.size.width - placeableWidth ) / 2 + this.particleRadius;
    var numParticlesInLayer = Math.floor( placeableWidth / ( this.particleRadius * 2 ) );

    // Loop, searching for open positions in the particle stack.
    for ( var layer = 0; layer <= highestOccupiedLayer + 1; layer++ ) {

      // Add all open locations in the current layer.
      for ( var positionInLayer = 0; positionInLayer < numParticlesInLayer; positionInLayer++ ) {
        var testPosition = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                        this.getYPositionForLayer( layer ) );
        if ( this.isPositionOpen( testPosition ) ) {

          // We found a location that is unoccupied.
          if ( layer == 0 || this.countSupportingParticles( testPosition ) == 2 ) {
            // This is a valid open location.
            openLocations.push( testPosition );
          }
        }
      }

      // Adjust variables for the next layer.
      numParticlesInLayer--;
      offsetFromBucketEdge += this.particleRadius;
      if ( numParticlesInLayer == 0 ) {
        // If the stacking pyramid is full, meaning that there are
        // no locations that are open within it, this algorithm
        // classifies the locations directly above the top of the
        // pyramid as being open.  This would result in a stack
        // of particles with a pyramid base.  So far, this hasn't
        // been a problem, but this limitation may limit
        // reusability of this algorithm.
        numParticlesInLayer = 1;
        offsetFromBucketEdge -= this.particleRadius;
      }
    }

    // Find the closest open location to the provided current location.
    // Only the X-component is used for this determination, because if
    // the Y-component is used the particles often appear to fall sideways
    // when released above the bucket, which just looks weird.
    var closestOpenLocation = openLocations[0] || Vector2.ZERO;

    _.each( openLocations, function ( location ) {
      if ( location.distance( position ) < closestOpenLocation.distance( position ) ) {
        // This location is closer.
        closestOpenLocation = location;
      }
    } )
    return closestOpenLocation;
  }

  SphereBucket.prototype.getYPositionForLayer = function ( layer ) {
    return this.position.y + this.yOffset - layer * this.particleRadius * 2 * 0.866;
  }

  /*
   * Determine whether a particle is 'dangling', i.e. hanging above an open
   * space in the stack of particles.  Dangling particles should fall.
   */
  SphereBucket.prototype.isDangling = function ( particle ) {
    var onBottomRow = particle.position.y === this.position.y + this.yOffset;
    return !onBottomRow && this.countSupportingParticles( particle.position.x, particle.position.y ) < 2;
  }

  SphereBucket.prototype.countSupportingParticles = function ( position ) {
    var count = 0;
    for ( var i = 0; i < this.particles.length; i++ ) {
      p = this.particles[i];
      if ( p.position.y > position.y && //must be in a lower layer (and larger y is further down on the page).
           p.position.distance( position ) < this.particleRadius * 3 ) {
        // Must be a supporting particle.
        count++;
      }
    }
    return count;
  }

  SphereBucket.prototype.relayoutBucketParticles = function () {
    do {
      for ( var i = 0; i < this.particles.length; i++ ) {
        var particleMoved = false;
        var particle = this.particles[i];
        if ( this.isDangling( particle ) ) {
          particle.position = this.getNearestOpenLocation( particle.position );
          particleMoved = true;
          break;
        }
      }
    } while ( particleMoved );
  }

  return SphereBucket;
} );
<|endoftext|>


<commit_before>// This module handles the construction of Sia-UI plugins.
import { List } from 'immutable'
import Path from 'path'
const remote = require('electron').remote
const fs = remote.require('fs')

// Create an icon element for a plugin button.
const createButtonIconElement = (path) => {
	const i = document.createElement('img')
	i.src = path
	i.className = 'pure-u icon'
	return i
}

// Create a text element for a plugin button.
const createButtonTextElement = (name) => {
	const t = document.createElement('div')
	t.innerText = name
	t.className = 'pure-u text'
	return t
}

// Construct a plugin view element from a plugin path and title
// use webview.preload to inject SiaAPI into the plugin's global namespace.
const createPluginElement = (markupPath, title) => {
	const elem = document.createElement('webview')
	elem.id = title + '-view'
	elem.className = 'webview'
	elem.src = markupPath
	// This is enabled for legacy plugin support.
	elem.nodeintegration = true
	elem.preload = Path.join('file://', Path.resolve('js/rendererjs/pluginapi.js'))
	return elem
}

// Set a plugin as the visible plugin
export const setCurrentPlugin = (pluginName) => {
	const currentElements = document.querySelectorAll('.current')
	for (const elem in currentElements) {
		if (typeof currentElements[elem].classList !== 'undefined') {
			currentElements[elem].classList.remove('current')
		}
	}
	const viewElem = document.getElementById(pluginName + '-view')
	if (viewElem !== null) {
		viewElem.classList.add('current')
	}
	const buttonElem = document.getElementById(pluginName + '-button')
	if (buttonElem !== null) {
		buttonElem.classList.add('current')
	}
}

// Construct a plugin button element from an icon path and title
const createPluginButtonElement = (iconPath, title) => {
	const elem = document.createElement('div')
	elem.id = title + '-button'
	elem.className = 'pure-u-1-1 button'
	elem.appendChild(createButtonIconElement(iconPath))
	elem.appendChild(createButtonTextElement(title))
	// On click, set all other buttons and plugins to non-current except this one.
	elem.onclick = () => setCurrentPlugin(title)
	return elem
}

// Get the name of a plugin from its path.
export const getPluginName = (pluginPath) => pluginPath.substring(pluginPath.lastIndexOf('/') + 1)

// loadPlugin constructs plugin view and plugin button elements
// and adds these elements to the main UI's mainbar/sidebar.
// Returns the plugin's main view element.
export const loadPlugin = (pluginPath) => {
	const name = getPluginName(pluginPath)
	const markupPath = Path.join(pluginPath, 'index.html')
	const iconPath = Path.join(pluginPath, 'assets', 'button.png')

	const viewElement = createPluginElement(markupPath, name)
	const buttonElement = createPluginButtonElement(iconPath, name)

	document.getElementById('sidebar').appendChild(buttonElement)
	document.getElementById('mainbar').appendChild(viewElement)

	return viewElement
}

// Scan a folder at `path` for plugins.
// Return a list of folder paths that have a valid plugin structure.
export const scanFolder = (path) => {
	let pluginFolders = List(fs.readdirSync(path))
	pluginFolders = pluginFolders.map((folder) => {
		const pluginPath = Path.join(path, folder)
		const markupPath = Path.join(pluginPath, 'index.html')
		try {
			fs.statSync(markupPath)
			return pluginPath
		} catch (e) {
			console.error('plugin ' + folder + ' has an invalid plugin structure')
		}
		return null
	})
	return pluginFolders
}
<commit_msg>map and then filter<commit_after>// This module handles the construction of Sia-UI plugins.
import { List } from 'immutable'
import Path from 'path'
const remote = require('electron').remote
const fs = remote.require('fs')

// Create an icon element for a plugin button.
const createButtonIconElement = (path) => {
	const i = document.createElement('img')
	i.src = path
	i.className = 'pure-u icon'
	return i
}

// Create a text element for a plugin button.
const createButtonTextElement = (name) => {
	const t = document.createElement('div')
	t.innerText = name
	t.className = 'pure-u text'
	return t
}

// Construct a plugin view element from a plugin path and title
// use webview.preload to inject SiaAPI into the plugin's global namespace.
const createPluginElement = (markupPath, title) => {
	const elem = document.createElement('webview')
	elem.id = title + '-view'
	elem.className = 'webview'
	elem.src = markupPath
	// This is enabled for legacy plugin support.
	elem.nodeintegration = true
	elem.preload = Path.join('file://', Path.resolve('js/rendererjs/pluginapi.js'))
	return elem
}

// Set a plugin as the visible plugin
export const setCurrentPlugin = (pluginName) => {
	const currentElements = document.querySelectorAll('.current')
	for (const elem in currentElements) {
		if (typeof currentElements[elem].classList !== 'undefined') {
			currentElements[elem].classList.remove('current')
		}
	}
	const viewElem = document.getElementById(pluginName + '-view')
	if (viewElem !== null) {
		viewElem.classList.add('current')
	}
	const buttonElem = document.getElementById(pluginName + '-button')
	if (buttonElem !== null) {
		buttonElem.classList.add('current')
	}
}

// Construct a plugin button element from an icon path and title
const createPluginButtonElement = (iconPath, title) => {
	const elem = document.createElement('div')
	elem.id = title + '-button'
	elem.className = 'pure-u-1-1 button'
	elem.appendChild(createButtonIconElement(iconPath))
	elem.appendChild(createButtonTextElement(title))
	// On click, set all other buttons and plugins to non-current except this one.
	elem.onclick = () => setCurrentPlugin(title)
	return elem
}

// Get the name of a plugin from its path.
export const getPluginName = (pluginPath) => pluginPath.substring(pluginPath.lastIndexOf('/') + 1)

// loadPlugin constructs plugin view and plugin button elements
// and adds these elements to the main UI's mainbar/sidebar.
// Returns the plugin's main view element.
export const loadPlugin = (pluginPath) => {
	const name = getPluginName(pluginPath)
	const markupPath = Path.join(pluginPath, 'index.html')
	const iconPath = Path.join(pluginPath, 'assets', 'button.png')

	const viewElement = createPluginElement(markupPath, name)
	const buttonElement = createPluginButtonElement(iconPath, name)

	document.getElementById('sidebar').appendChild(buttonElement)
	document.getElementById('mainbar').appendChild(viewElement)

	return viewElement
}

// Scan a folder at `path` for plugins.
// Return a list of folder paths that have a valid plugin structure.
export const scanFolder = (path) => {
	let pluginFolders = List(fs.readdirSync(path))
	pluginFolders = pluginFolders.map((folder) => Path.join(path, folder))
	pluginFolders = pluginFolders.filter((pluginPath) => {
		const markupPath = Path.join(pluginPath, 'index.html')
		try {
			fs.statSync(markupPath)
			return true
		} catch (e) {
			console.error('plugin ' + path + ' has an invalid structure')
		}
		return false
	})
	return pluginFolders
}
<|endoftext|>


<commit_before>'use strict';

// external deps
var ObjectId = require('mongodb').ObjectId;

// internal deps
require('mongodb-toolkit');
var BateeqModels = require('bateeq-models');
var map = BateeqModels.map;

var InventoryMovement = BateeqModels.inventory.InventoryMovement;

module.exports = class InventoryMovementManager {
    constructor(db, user) {
        this.db = db;
        this.user = user;
        this.inventoryMovementCollection = this.db.use(map.inventory.InventoryMovement);

        var StorageManager = require('../master/storage-manager');
        this.storageManager = new StorageManager(db, user);

        var ItemManager = require('../master/item-manager');
        this.itemManager = new ItemManager(db, user);
    }

    read(paging) {
        var _paging = Object.assign({
            page: 1,
            size: 20,
            order: '_id',
            asc: true
        }, paging);

        return new Promise((resolve, reject) => {
            var deleted = {
                _deleted: false
            };
            var query = _paging.keyword ? {
                '$and': [deleted]
            } : deleted;

            if (_paging.keyword) {
                var regex = new RegExp(_paging.keyword, "i");
                var filterItemName = {
                    'item.name': {
                        '$regex': regex
                    }
                };
                var filterStorageName = {
                    'storage.name': {
                        '$regex': regex
                    }
                };
                var $or = {
                    '$or': [filterItemName, filterStorageName]
                };

                query['$and'].push($or);
            }

            this.inventoryMovementCollection
                .where(query)
                .page(_paging.page, _paging.size)
                .orderBy(_paging.order, _paging.asc)
                .execute()
                .then(inventoryMovements => {
                    resolve(inventoryMovements);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    readByStorageIdAndItemId(storageId, itemId, paging) {
        var _paging = Object.assign({
            page: 1,
            size: 20,
            order: '_id',
            asc: true
        }, paging);

        return new Promise((resolve, reject) => {
            var deleted = {
                _deleted: false
            };
            var storage = {
                storageId: new ObjectId(storageId)
            };
            var item = {
                itemId: new ObjectId(itemId)
            };
            var query = {
                '$and': [deleted, storage, item]
            };

            if (_paging.keyword) {
                var regex = new RegExp(_paging.keyword, "i");
                var filterItemName = {
                    'item.name': {
                        '$regex': regex
                    }
                };
                var filterStorageName = {
                    'storage.name': {
                        '$regex': regex
                    }
                };
                var $or = {
                    '$or': [filterItemName, filterStorageName]
                };

                query['$and'].push($or);
            }

            this.inventoryMovementCollection
                .where(query)
                .page(_paging.page, _paging.size)
                .orderBy(_paging.order, _paging.asc)
                .execute()
                .then(inventoryMovements => {
                    resolve(inventoryMovements);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getSingleById(id) {
        return new Promise((resolve, reject) => {
            if (id === '')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                _deleted: false
            };
            this.getSingleByQuery(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getSingleByIdOrDefault(id) {
        return new Promise((resolve, reject) => {
            if (id === '')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                _deleted: false
            };
            this.getSingleByQueryOrDefault(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getByStorageIdAndItemIdAndId(storageId, itemId, id) {
        return new Promise((resolve, reject) => {
            if (id === ''|| storageId ==='' || itemId ==='')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                storageId: new ObjectId(storageId),
                itemId: new ObjectId(itemId),
                _deleted: false
            };
            this.getSingleByQuery(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getByStorageIdAndItemIdAndIdOrDefault(storageId, itemId, id) {
        return new Promise((resolve, reject) => {
            if (id === ''|| storageId ==='' || itemId ==='')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                storageId: new ObjectId(storageId),
                itemId: new ObjectId(itemId),
                _deleted: false
            };
            this.getSingleByQueryOrDefault(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getSingleByQuery(query) {
        return new Promise((resolve, reject) => {
            this.inventoryMovementCollection
                .single(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        })
    }

    getSingleByQueryOrDefault(query) {
        return new Promise((resolve, reject) => {
            this.inventoryMovementCollection
                .singleOrDefault(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        })
    }

    create(inventoryMovement) {
        return new Promise((resolve, reject) => {
            this._validate(inventoryMovement)
                .then(validInventoryMovement => {
                    validInventoryMovement._createdDate = new Date();
                    this.inventoryMovementCollection.insert(validInventoryMovement)
                        .then(id => {
                            resolve(id);
                        })
                        .catch(e => {
                            reject(e);
                        })
                })
                .catch(e => {
                    reject(e);
                })
        });
    }

    update(inventoryMovement) {
        return new Promise((resolve, reject) => {
            this._validate(inventoryMovement)
                .then(validInventoryMovement => {
                    this.inventoryMovementCollection.update(validInventoryMovement)
                        .then(id => {
                            resolve(id);
                        })
                        .catch(e => {
                            reject(e);
                        })
                })
                .catch(e => {
                    reject(e);
                })
        });
    }

    delete(inventoryMovement) {
        return new Promise((resolve, reject) => {
            this._validate(inventoryMovement)
                .then(validInventoryMovement => {
                    validInventoryMovement._deleted = true;
                    this.inventoryMovementCollection.update(validInventoryMovement)
                        .then(id => {
                            resolve(id);
                        })
                        .catch(e => {
                            reject(e);
                        })
                })
                .catch(e => {
                    reject(e);
                })
        });
    }

    _validate(inventoryMovement) {
        var errors = {};
        return new Promise((resolve, reject) => {
            var valid = inventoryMovement;
            var getStorage = this.storageManager.getSingleById(inventoryMovement.storageId);
            var getItem;

            if (inventoryMovement.itemId) {
                getItem = this.itemManager.getSingleById(inventoryMovement.itemId);
            } else {
                getItem = this.productManager.getSingleById(inventoryMovement.productId);
            }

            Promise.all([getStorage, getItem])
                .then(results => {
                    var storage = results[0];
                    var item = results[1];

                    if (!valid.storageId || valid.storageId == '')
                        errors["storageId"] = "storageId is required";
                    if (!storage) {
                        errors["storageId"] = "storageId not found";
                    }
                    else {
                        valid.storageId = storage._id;
                        valid.storage = storage;
                    }

                    if (valid.itemId) {
                        if (!valid.itemId || valid.itemId == '')
                            errors["itemId"] = "itemId is required";
                        if (!item) {
                            errors["itemId"] = "itemId not found";
                        }
                        else {
                            valid.itemId = item._id;
                            valid.item = item;
                        }
                    } else {
                        if (!valid.productId || valid.itemId == '')
                            errors["itemId"] = "productId is required";
                        if (!item) {
                            errors["itemId"] = "item not found";
                        }
                        else {
                            valid.itemId = item._id;
                            valid.item = item;
                        }
                    }

                    if (valid.type) {
                        if (!valid.type || valid.type == '')
                            errors["type"] = "type is required";
                    }

                    if (valid.date) {
                        if (valid.date == undefined || (valid.date && valid.date == '')) {
                            errors["date"] = "date is required";
                        }
                    }

                    if (valid.quantity == undefined || (valid.quantity && valid.quantity == '')) {
                        errors["quantity"] = "quantity is required";
                    }
                    // else if (parseInt(valid.quantity) <= 0) {
                    //     errors["quantity"] = "quantity must be greater than 0";
                    // }

                    if (valid.before) {
                        if (valid.before == undefined || (valid.before && valid.before == '')) {
                            errors["before"] = "before is required";
                        }
                        else if (parseInt(valid.before) < 0) {
                            errors["before"] = "before must be greater than 0";
                        }
                    }

                    if (valid.after) {
                        if (valid.after == undefined || (valid.after && valid.after == '')) {
                            errors["after"] = "after is required";
                        }
                        else if (parseInt(valid.after) < 0) {
                            errors["after"] = "after must be greater than 0";
                        }
                    }

                    // 2c. begin: check if data has any error, reject if it has.
                    for (var prop in errors) {
                        var ValidationError = require('module-toolkit').ValidationError;
                        reject(new ValidationError('data does not pass validation', errors));
                    }

                    if (valid.stamp) {
                        valid.stamp(this.user.username, 'manager');
                    }

                    resolve(valid)
                })
                .catch(e => {
                    reject(e);
                });
        });
    }
};<commit_msg>update: inventory movement<commit_after>'use strict';

// external deps
var ObjectId = require('mongodb').ObjectId;

// internal deps
require('mongodb-toolkit');
var BateeqModels = require('bateeq-models');
var map = BateeqModels.map;

var InventoryMovement = BateeqModels.inventory.InventoryMovement;

module.exports = class InventoryMovementManager {
    constructor(db, user) {
        this.db = db;
        this.user = user;
        this.inventoryMovementCollection = this.db.use(map.inventory.InventoryMovement);

        var StorageManager = require('../master/storage-manager');
        this.storageManager = new StorageManager(db, user);

        var ItemManager = require('../master/item-manager');
        this.itemManager = new ItemManager(db, user);

        var ProductManager = require('../master/product-manager');
        this.productManager = new ProductManager(db, user);
    }

    read(paging) {
        var _paging = Object.assign({
            page: 1,
            size: 20,
            order: '_id',
            asc: true
        }, paging);

        return new Promise((resolve, reject) => {
            var deleted = {
                _deleted: false
            };
            var query = _paging.keyword ? {
                '$and': [deleted]
            } : deleted;

            if (_paging.keyword) {
                var regex = new RegExp(_paging.keyword, "i");
                var filterItemName = {
                    'item.name': {
                        '$regex': regex
                    }
                };
                var filterStorageName = {
                    'storage.name': {
                        '$regex': regex
                    }
                };
                var $or = {
                    '$or': [filterItemName, filterStorageName]
                };

                query['$and'].push($or);
            }

            this.inventoryMovementCollection
                .where(query)
                .page(_paging.page, _paging.size)
                .orderBy(_paging.order, _paging.asc)
                .execute()
                .then(inventoryMovements => {
                    resolve(inventoryMovements);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    readByStorageIdAndItemId(storageId, itemId, paging) {
        var _paging = Object.assign({
            page: 1,
            size: 20,
            order: '_id',
            asc: true
        }, paging);

        return new Promise((resolve, reject) => {
            var deleted = {
                _deleted: false
            };
            var storage = {
                storageId: new ObjectId(storageId)
            };
            var item = {
                itemId: new ObjectId(itemId)
            };
            var query = {
                '$and': [deleted, storage, item]
            };

            if (_paging.keyword) {
                var regex = new RegExp(_paging.keyword, "i");
                var filterItemName = {
                    'item.name': {
                        '$regex': regex
                    }
                };
                var filterStorageName = {
                    'storage.name': {
                        '$regex': regex
                    }
                };
                var $or = {
                    '$or': [filterItemName, filterStorageName]
                };

                query['$and'].push($or);
            }

            this.inventoryMovementCollection
                .where(query)
                .page(_paging.page, _paging.size)
                .orderBy(_paging.order, _paging.asc)
                .execute()
                .then(inventoryMovements => {
                    resolve(inventoryMovements);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getSingleById(id) {
        return new Promise((resolve, reject) => {
            if (id === '')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                _deleted: false
            };
            this.getSingleByQuery(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getSingleByIdOrDefault(id) {
        return new Promise((resolve, reject) => {
            if (id === '')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                _deleted: false
            };
            this.getSingleByQueryOrDefault(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getByStorageIdAndItemIdAndId(storageId, itemId, id) {
        return new Promise((resolve, reject) => {
            if (id === ''|| storageId ==='' || itemId ==='')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                storageId: new ObjectId(storageId),
                itemId: new ObjectId(itemId),
                _deleted: false
            };
            this.getSingleByQuery(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getByStorageIdAndItemIdAndIdOrDefault(storageId, itemId, id) {
        return new Promise((resolve, reject) => {
            if (id === ''|| storageId ==='' || itemId ==='')
                resolve(null);
            var query = {
                _id: new ObjectId(id),
                storageId: new ObjectId(storageId),
                itemId: new ObjectId(itemId),
                _deleted: false
            };
            this.getSingleByQueryOrDefault(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        });
    }

    getSingleByQuery(query) {
        return new Promise((resolve, reject) => {
            this.inventoryMovementCollection
                .single(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        })
    }

    getSingleByQueryOrDefault(query) {
        return new Promise((resolve, reject) => {
            this.inventoryMovementCollection
                .singleOrDefault(query)
                .then(inventoryMovement => {
                    resolve(inventoryMovement);
                })
                .catch(e => {
                    reject(e);
                });
        })
    }

    create(inventoryMovement) {
        return new Promise((resolve, reject) => {
            this._validate(inventoryMovement)
                .then(validInventoryMovement => {
                    validInventoryMovement._createdDate = new Date();
                    this.inventoryMovementCollection.insert(validInventoryMovement)
                        .then(id => {
                            resolve(id);
                        })
                        .catch(e => {
                            reject(e);
                        })
                })
                .catch(e => {
                    reject(e);
                })
        });
    }

    update(inventoryMovement) {
        return new Promise((resolve, reject) => {
            this._validate(inventoryMovement)
                .then(validInventoryMovement => {
                    this.inventoryMovementCollection.update(validInventoryMovement)
                        .then(id => {
                            resolve(id);
                        })
                        .catch(e => {
                            reject(e);
                        })
                })
                .catch(e => {
                    reject(e);
                })
        });
    }

    delete(inventoryMovement) {
        return new Promise((resolve, reject) => {
            this._validate(inventoryMovement)
                .then(validInventoryMovement => {
                    validInventoryMovement._deleted = true;
                    this.inventoryMovementCollection.update(validInventoryMovement)
                        .then(id => {
                            resolve(id);
                        })
                        .catch(e => {
                            reject(e);
                        })
                })
                .catch(e => {
                    reject(e);
                })
        });
    }

    _validate(inventoryMovement) {
        var errors = {};
        return new Promise((resolve, reject) => {
            var valid = inventoryMovement;
            var getStorage = this.storageManager.getSingleById(inventoryMovement.storageId);
            var getItem;

            if (ObjectId.isValid(inventoryMovement.itemId)) {
                getItem = this.itemManager.getSingleById(inventoryMovement.itemId);
            } else if (ObjectId.isValid(inventoryMovement.productId)) {
                getItem = this.productManager.getSingleById(inventoryMovement.productId);
            }

            Promise.all([getStorage, getItem])
                .then(results => {
                    var storage = results[0];
                    var item = results[1];

                    if (!valid.storageId || valid.storageId == '')
                        errors["storageId"] = "storageId is required";
                    if (!storage) {
                        errors["storageId"] = "storageId not found";
                    }
                    else {
                        valid.storageId = storage._id;
                        valid.storage = storage;
                    }

                    if (ObjectId.isValid(valid.itemId)) {
                        if (!valid.itemId || valid.itemId == '')
                            errors["itemId"] = "itemId is required";
                        if (!item) {
                            errors["itemId"] = "itemId not found";
                        }
                        else {
                            valid.itemId = item._id;
                            valid.item = item;
                        }
                    } else {
                        if (!valid.productId || valid.productId == '')
                            errors["productId"] = "productId is required";
                        if (!item) {
                            errors["productId"] = "product not found";
                        }
                        else {
                            valid.productId = item._id;
                            valid.product = item;
                        }
                    }

                    if (valid.type) {
                        if (!valid.type || valid.type == '')
                            errors["type"] = "type is required";
                    }

                    if (valid.date) {
                        if (valid.date == undefined || (valid.date && valid.date == '')) {
                            errors["date"] = "date is required";
                        }
                    }

                    if (valid.quantity == undefined || (valid.quantity && valid.quantity == '')) {
                        errors["quantity"] = "quantity is required";
                    }
                    // else if (parseInt(valid.quantity) <= 0) {
                    //     errors["quantity"] = "quantity must be greater than 0";
                    // }

                    if (valid.before) {
                        if (valid.before == undefined || (valid.before && valid.before == '')) {
                            errors["before"] = "before is required";
                        }
                        else if (parseInt(valid.before) < 0) {
                            errors["before"] = "before must be greater than 0";
                        }
                    }

                    if (valid.after) {
                        if (valid.after == undefined || (valid.after && valid.after == '')) {
                            errors["after"] = "after is required";
                        }
                        else if (parseInt(valid.after) < 0) {
                            errors["after"] = "after must be greater than 0";
                        }
                    }

                    // 2c. begin: check if data has any error, reject if it has.
                    for (var prop in errors) {
                        var ValidationError = require('module-toolkit').ValidationError;
                        reject(new ValidationError('data does not pass validation', errors));
                    }

                    if (!valid.stamp) {
                        valid = new InventoryMovement(valid);
                    }

                    valid.stamp(this.user.username, 'manager');
                    resolve(valid)
                })
                .catch(e => {
                    reject(e);
                });
        });
    }
};<|endoftext|>


<commit_before>function IntroRenderer(userCourses, popover){
    this.init = function() {
        this.userCourses = userCourses;
        this.addButton();
        this.popover = popover;
    }

    this.addButton = function() {
        var self = this;
        var button = $('<button>').attr('id', 'intro-button')
            .addClass('btn btn glyphicon glyphicon-question-sign');
        button[0].addEventListener("click", function (evt) {
            console.log('click');
            var tripArray = []
            var currentTab = $('.tab-pane.active')[0].id;
            switch(currentTab) {
            case "configure":
                tripArray.push(self.toolbarRenderer(0, "Configure your settings"));
                self.configureRenderer(tripArray);
                break;
            case "waived":
                self.misc(tripArray);
                tripArray.push(self.toolbarRenderer(1, "Mark courses for which you have received credit."));
                self.waiveRenderer(tripArray);
                break;
            case "taken":
                self.misc(tripArray);
                tripArray.push(self.toolbarRenderer(2, "Mark courses that you have already completed."));
                self.completedRenderer(tripArray);
                break;
            case "selected":
                self.misc(tripArray);
                tripArray.push(self.toolbarRenderer(3, "Mark courses that you wish to take."));
                self.selectedRenderer(tripArray);
                break;
            case "schedules":
                tripArray.push(self.toolbarRenderer(4, "Render a schedule with your current criteria."));
                self.scheduleRenderer(tripArray);
                break;
            }
            options = {  delay : -1,
                         //canGoPrev: false,
                         //prevLabel: "",
                         animation: undefined,
                         onTripEnd : function() {
                             $('.trip-block').remove();
                         }
                      }

            this.trip = new Trip(tripArray, options);
            this.trip.start();
        }, false);
        // //self.takeTrip();
        //     // }
        // }, false);
        $('#intro').append(button);

    }

    this.toolbarRenderer = function(tab, content) {
        console.log(tab);
        tabIndex =["Configure", "Waived", "Completed", "Selected", "Schedule"]
        console.log($('a:contains("' + tabIndex[tab] + '")'));
        var trip =
            {
                sel : $('a:contains("' + tabIndex[tab] + '")'),
                content : "" + content,
                position: "screen-ne",
                showNavigation: true,
                expose: true,
                callback: function() {
                    $('#rootwizard').bootstrapWizard('show',tab);
                }
            }
        return trip;
    }
    this.availableRenderer = function(trip, context) {
        self = this;
        trip.push({
            sel :  $('#' + context + '-available'),
            content : "All courses that can be " + context + " are shown in the course selection list.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel :  $('#available-groups,#available-groups, #waived-groups'),
            content : "You can filter courses based on their level by clicking on one of the level filter buttons.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
            // callback: function(callback) {
            //     var courseNumber = $('#' + context + '-group > button')[0].id;
            //     var course = self.userCourses.getCourse(courseNumber);
            //     self.popover.renderPopover(course,'selected');
            // }
        });

        trip.push({
            sel :  $('#6900A, #6900B, #5880A, 5880B, #5890A, 5890B, #4880A, 4880B'),
            content : "All courses are considered to be 3 credit hours each.
Courses that can be repeated for credit are duplicated in the list.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
            callback: function(callback) {
                var courseNumber = $('#' + context + '-group > button')[0].id;
                var course = self.userCourses.getCourse(courseNumber);
                self.popover.renderPopover(course,'selected');
            }
        });

        trip.push({
            sel : $('#' + context + '-group'),
            content : 'If you click on a course you will see the course title.
 You can also perform any action available for the course in the current context by clicking on the corresponding action button.',
            position: "screen-ne",
            showNavigation: true,
            callback: function() {
                self.popover.destroyPopover();
            },
            expose: true
        });
        trip.push({
            sel : $('#' + context + '-group'),
            content : 'Alternatively, clicking on the course a second time will perform the default action in the current context.',
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

    }

    this.configureRenderer = function(trip) {
        trip.push({
            sel : $('.int-student'),
            content : "If you are a international student, select this option.
 This will schedule 3 courses per semester in each semester except the last one.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.restricted-status'),
            content : "If you have been admitted on restricted status, select this option.
 This will make prerequisite undergraduate courses available for scheduling.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.courses-per-sem'),
            content : "For your convenience the maximum number courses you wish to take in any semester
 and the minimum number of courses you will need to complete your degree are linked.
 If you know what you are doing you can unlink them and set them seperately.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.start-date'),
            content : "Set this value if you want to set your starting date for a different semester.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.night-only'),
            content : "Select this option if you only want to schedule night classes.
This feature is only accurate for the coming semester.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.days-of-week'),
            content : "Select the days of the week you prefer to have classes.
This feature is only accurate for the coming semester.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.required-nr-courses'),
            content : "Change the number of courses required to graduate.
This is a developer option and in general should not be changed",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

    }
    this.waiveRenderer = function(trip) {
        var self = this;
        this.availableRenderer(trip, "waived");

        trip.push({
            sel :  $('#waived-waived'),
            content : "If you apply the waive action, the course will be waived. If you apply another action the new course status will only be visible in the corresponding tab.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
        });

    }

    this.maxLines = function(trip) {
        trip.push({
            sel : $('.max-4000'),
            content : 'The red line shows the maximum number of courses that can be taken for credit at the 4000 level.',
            position: "screen-nw",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $(".min-5000, .min-6000"),
            content : 'The green lines shows the minimum number of courses that must be taken at the 5000 and 6000 levels.',
            position: "screen-nw",
            showNavigation: true,
            expose: true
        });
    }

    this.completedRenderer = function(trip) {
        this.availableRenderer(trip, "taken");
        trip.push({
            sel : $('.taken'),
            content : "If you apply the complete action, the course will be moved to its corresponding completed list.
 If you apply another action the new course status will only be visible in the corresponding tab.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
        });
        this.maxLines(trip);

    }

    this.selectedRenderer = function(trip) {
        var self = this;
        this.availableRenderer(trip, "selected");
        this.maxLines(trip);
        trip.push({
            sel : $('.selected'),
            content : "You do not need to select all 10 courses.
If you select only a subset of courses the scheduler will fill the schedule with courses that satisfy the degree criteria.",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('#4250, #4760, #5130, #5500, #5700'),
            content : "Core courses are always either selected, completed or waived, they can not be unselected",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('.selected'),
            content : "Waived courses are not shown anywhere, as they are effectively eliminated from selection",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('.selected, a:contains("Schedule")'),
            content : "Completed courses are shown with dashed lines, and drop to the bottom of the list.
They decrease the number of courses required to complete your degree",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        return trip;
    }

    this.misc = function(trip) {
        trip.push({
            sel : $('.glyphicon-floppy-open'),
            content : "Load previously saved data from a JSON file.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('.glyphicon-floppy-save'),
            content : "Save your data in JSON format.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('.glyphicon-trash'),
            content : "Reset your current session. All unsaved changes will be lost.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('#searchbar'),
            content : "You can search for a course based on the course number or title.
 You can also use regular expression syntax to do more advanced searches.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('#searchbutton'),
            content : "To complete your search you may press return or click the search button.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('#resetbutton'),
            content : "To clear the search results you can press the search reset button.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });
    }

    this.scheduleRenderer = function(trip) {
        var self = this;
        trip.push({
            sel : $('.table-header'),
            content : "The schedule is rendered for the number of semesters you configured, starting from the semester you configured.",
            position: "screen-s",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('.day-time'),
            content : "If available, the scheduler will show what day a course is being presented, and whether the course is a day course or an evening course.",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });
        trip.push({
            sel : $('.prereq'),
            content : "Courses that are scheduled with a pre-requirement are highlighted.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
        });

        return trip;
    }

    this.takeTrip = function() {
        var tripArray = []
        tripArray.push(this.toolbarRenderer(0, "The Configuration Tab"));
        this.configureRenderer(tripArray);
        tripArray.push(this.toolbarRenderer(1, "The Waived Tab "));
        this.waiveRenderer(tripArray);
        tripArray.push(this.toolbarRenderer(2, "The Completed Tab "));
        this.completedRenderer(tripArray);
        tripArray.push(this.toolbarRenderer(3, "The Selected Tab"));
        this.selectedRenderer(tripArray);

        options = {  delay : -1,
                     //canGoPrev: false,
                     //prevLabel: "",
                     animation: undefined,
                     onTripEnd : function() {
                         $('.trip-block').remove();
                     }
                  }

        this.trip = new Trip(tripArray, options);
        this.trip.start();

    }

    this.init();
}
<commit_msg>updated intro<commit_after>function IntroRenderer(userCourses, popover){
    this.init = function() {
        this.userCourses = userCourses;
        this.addButton();
        this.popover = popover;
    }

    this.addButton = function() {
        var self = this;
        var button = $('<button>').attr('id', 'intro-button')
            .addClass('btn btn glyphicon glyphicon-question-sign');
        button[0].addEventListener("click", function (evt) {
            console.log('click');
            var tripArray = []
            var currentTab = $('.tab-pane.active')[0].id;
            switch(currentTab) {
            case "configure":
                self.navRender(tripArray);
                self.toolbarRenderer(tripArray, 0, "Configure your settings");
                self.configureRenderer(tripArray);
                break;
            case "waived":
                self.navRender(tripArray);
                self.misc(tripArray);
                self.toolbarRenderer(tripArray, 1, "Mark courses for which you have received credit.");
                self.waiveRenderer(tripArray);
                break;
            case "taken":
                self.navRender(tripArray);
                self.misc(tripArray);
                self.toolbarRenderer(tripArray, 2, "Mark courses that you have already completed.");
                self.completedRenderer(tripArray);
                break;
            case "selected":
                self.navRender(tripArray);
                self.misc(tripArray);
                self.toolbarRenderer(tripArray, 3, "Mark courses that you wish to take.");
                self.selectedRenderer(tripArray);
                break;
            case "schedules":
                self.navRender(tripArray);
                self.toolbarRenderer(tripArray, 4, "Render a schedule with your current criteria.");
                self.scheduleRenderer(tripArray);
                break;
            }
            options = {  delay : -1,
                         //canGoPrev: false,
                         //prevLabel: "",
                         animation: undefined,
                         onTripEnd : function() {
                             $('.trip-block').remove();
                         }
                      }

            this.trip = new Trip(tripArray, options);
            this.trip.start();
        }, false);

        // //self.takeTrip();
        //     // }
        // }, false);
        $('#intro').append(button);

    }

    this.navRender = function(trip) {
        trip.push({
            sel :  $('body'),
            content : "You can use the left and right arrow keys to navigate the introduction.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel :  $('body'),
            content : "The introduction is context sensitive. It introduces the current selected tab only. You can run it again from another tab.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        return trip
    }

    this.toolbarRenderer = function(trip, tab, content) {
        console.log(tab);
        tabIndex =["Configure", "Waived", "Completed", "Selected", "Schedule"]
        console.log($('a:contains("' + tabIndex[tab] + '")'));
        trip.push(
            {
                sel : $('a:contains("' + tabIndex[tab] + '")'),
                content : "" + content,
                position: "screen-ne",
                showNavigation: true,
                expose: true,
                callback: function() {
                    $('#rootwizard').bootstrapWizard('show',tab);
                }
            });
        return trip;
    }
    this.availableRenderer = function(trip, context) {
        self = this;
        trip.push({
            sel :  $('#' + context + '-available'),
            content : "All courses that can be " + context + " are shown in the course selection list.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel :  $('#available-groups,#available-groups, #waived-groups'),
            content : "You can filter courses based on their level by clicking on one of the level filter buttons.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
            // callback: function(callback) {
            //     var courseNumber = $('#' + context + '-group > button')[0].id;
            //     var course = self.userCourses.getCourse(courseNumber);
            //     self.popover.renderPopover(course,'selected');
            // }
        });

        trip.push({
            sel :  $('#6900A, #6900B, #5880A, 5880B, #5890A, 5890B, #4880A, 4880B'),
            content : "All courses are considered to be 3 credit hours each.
Courses that can be repeated for credit are duplicated in the list.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
            callback: function(callback) {
                var courseNumber = $('#' + context + '-group > button')[0].id;
                var course = self.userCourses.getCourse(courseNumber);
                self.popover.renderPopover(course,'selected');
            }
        });

        trip.push({
            sel : $('#' + context + '-group'),
            content : 'If you click on a course you will see the course title.
 You can also perform any action available for the course in the current context by clicking on the corresponding action button.',
            position: "screen-ne",
            showNavigation: true,
            callback: function() {
                self.popover.destroyPopover();
            },
            expose: true
        });
        trip.push({
            sel : $('#' + context + '-group'),
            content : 'Alternatively, clicking on the course a second time will perform the default action in the current context.',
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

    }

    this.configureRenderer = function(trip) {
        trip.push({
            sel : $('.int-student'),
            content : "If you are a international student, select this option.
 This will schedule 3 courses per semester in each semester except the last one.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.restricted-status'),
            content : "If you have been admitted on restricted status, select this option.
 This will make prerequisite undergraduate courses available for scheduling.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.courses-per-sem'),
            content : "For your convenience the maximum number courses you wish to take in any semester
 and the minimum number of courses you will need to complete your degree are linked.
 If you know what you are doing you can unlink them and set them seperately.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.start-date'),
            content : "Set this value if you want to set your starting date for a different semester.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.night-only'),
            content : "Select this option if you only want to schedule night classes.
This feature is only accurate for the coming semester.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.days-of-week'),
            content : "Select the days of the week you prefer to have classes.
This feature is only accurate for the coming semester.",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $('.required-nr-courses'),
            content : "Change the number of courses required to graduate.
This is a developer option and in general should not be changed",
            position: "screen-ne",
            showNavigation: true,
            expose: true
        });

    }
    this.waiveRenderer = function(trip) {
        var self = this;
        this.availableRenderer(trip, "waived");

        trip.push({
            sel :  $('#waived-waived'),
            content : "If you apply the waive action, the course will be waived. If you apply another action the new course status will only be visible in the corresponding tab.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
        });

    }

    this.maxLines = function(trip) {
        trip.push({
            sel : $('.max-4000'),
            content : 'The red line shows the maximum number of courses that can be taken for credit at the 4000 level.',
            position: "screen-nw",
            showNavigation: true,
            expose: true
        });
        trip.push({
            sel : $(".min-5000, .min-6000"),
            content : 'The green lines shows the minimum number of courses that must be taken at the 5000 and 6000 levels.',
            position: "screen-nw",
            showNavigation: true,
            expose: true
        });
    }

    this.completedRenderer = function(trip) {
        this.availableRenderer(trip, "taken");
        trip.push({
            sel : $('.taken'),
            content : "If you apply the complete action, the course will be moved to its corresponding completed list.
 If you apply another action the new course status will only be visible in the corresponding tab.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
        });
        this.maxLines(trip);

    }

    this.selectedRenderer = function(trip) {
        var self = this;
        this.availableRenderer(trip, "selected");
        this.maxLines(trip);
        trip.push({
            sel : $('.selected'),
            content : "You do not need to select all 10 courses.
If you select only a subset of courses the scheduler will fill the schedule with courses that satisfy the degree criteria.",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('#4250, #4760, #5130, #5500, #5700'),
            content : "Core courses are always either selected, completed or waived, they can not be unselected",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('.selected'),
            content : "Waived courses are not shown anywhere, as they are effectively eliminated from selection",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('.selected, a:contains("Schedule")'),
            content : "Completed courses are shown with dashed lines, and drop to the bottom of the list.
They decrease the number of courses required to complete your degree",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        return trip;
    }

    this.misc = function(trip) {
        trip.push({
            sel : $('.glyphicon-floppy-open'),
            content : "Load previously saved data from a JSON file.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('.glyphicon-floppy-save'),
            content : "Save your data in JSON format.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('.glyphicon-trash'),
            content : "Reset your current session. All unsaved changes will be lost.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('#searchbar'),
            content : "You can search for a course based on the course number or title.
 You can also use regular expression syntax to do more advanced searches.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('#searchbutton'),
            content : "To complete your search you may press return or click the search button.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });

        trip.push({
            sel : $('#resetbutton'),
            content : "To clear the search results you can press the search reset button.",
            position: "screen-sw",
            showNavigation: true,
            expose: true
        });
    }

    this.scheduleRenderer = function(trip) {
        var self = this;
        trip.push({
            sel : $('.table-header'),
            content : "The schedule is rendered for the number of semesters you configured, starting from the semester you configured.",
            position: "screen-s",
            showNavigation: true,
            expose: true,
        });

        trip.push({
            sel : $('.day-time'),
            content : "If available, the scheduler will show what day a course is being presented, and whether the course is a day course or an evening course.",
            position: "screen-nw",
            showNavigation: true,
            expose: true,
        });
        trip.push({
            sel : $('.prereq'),
            content : "Courses that are scheduled with a pre-requirement are highlighted.",
            position: "screen-ne",
            showNavigation: true,
            expose: true,
        });

        return trip;
    }

    // this.takeTrip = function() {
    //     var tripArray = []
    //     tripArray.push(this.toolbarRenderer(0, "The Configuration Tab"));
    //     this.configureRenderer(tripArray);
    //     tripArray.push(this.toolbarRenderer(1, "The Waived Tab "));
    //     this.waiveRenderer(tripArray);
    //     tripArray.push(this.toolbarRenderer(2, "The Completed Tab "));
    //     this.completedRenderer(tripArray);
    //     tripArray.push(this.toolbarRenderer(3, "The Selected Tab"));
    //     this.selectedRenderer(tripArray);

    //     options = {  delay : -1,
    //                  //canGoPrev: false,
    //                  //prevLabel: "",
    //                  animation: undefined,
    //                  onTripEnd : function() {
    //                      $('.trip-block').remove();
    //                  }
    //               }

    //     this.trip = new Trip(tripArray, options);
    //     this.trip.start();

    // }

    this.init();
}
<|endoftext|>


<commit_before>// @flow

import type {
  NavigationScreenProp,
  NavigationLeafRoute,
  NavigationScene,
  NavigationTransitionProps,
} from 'react-navigation';
import {
  type Media,
  mediaPropType,
  type Dimensions,
  dimensionsPropType,
} from 'lib/types/media-types';
import type { AppState } from '../redux-setup';
import { type VerticalBounds, verticalBoundsPropType } from './vertical-bounds';

import * as React from 'react';
import {
  View,
  StyleSheet,
  TouchableWithoutFeedback,
  Animated,
  TouchableHighlight,
  Platform,
} from 'react-native';
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/Feather';

import { connect } from 'lib/utils/redux-utils';

import {
  contentVerticalOffset,
  contentBottomOffset,
  dimensionsSelector,
} from '../selectors/dimension-selectors';
import Multimedia from './multimedia.react';
import ConnectedStatusBar from '../connected-status-bar.react';

type LayoutCoordinates = $ReadOnly<{|
  x: number,
  y: number,
  width: number,
  height: number,
|}>;
type NavProp = NavigationScreenProp<{|
  ...NavigationLeafRoute,
  params: {|
    media: Media,
    initialCoordinates: LayoutCoordinates,
    verticalBounds: VerticalBounds,
  |},
|}>;

type Props = {|
  navigation: NavProp,
  scene: NavigationScene,
  transitionProps: NavigationTransitionProps,
  // Redux state
  screenDimensions: Dimensions,
|};
class MultimediaModal extends React.PureComponent<Props> {

  static propTypes = {
    navigation: PropTypes.shape({
      state: PropTypes.shape({
        params: PropTypes.shape({
          media: mediaPropType.isRequired,
          initialCoordinates: PropTypes.shape({
            x: PropTypes.number.isRequired,
            y: PropTypes.number.isRequired,
            width: PropTypes.number.isRequired,
            height: PropTypes.number.isRequired,
          }).isRequired,
          verticalBounds: verticalBoundsPropType.isRequired,
        }).isRequired,
      }).isRequired,
      goBack: PropTypes.func.isRequired,
    }).isRequired,
    transitionProps: PropTypes.object.isRequired,
    scene: PropTypes.object.isRequired,
    screenDimensions: dimensionsPropType.isRequired,
  };

  get screenDimensions(): Dimensions {
    const { screenDimensions } = this.props;
    if (contentVerticalOffset === 0) {
      return screenDimensions;
    }
    const { height, width } = screenDimensions;
    return { height: height - contentVerticalOffset, width };
  }

  get imageDimensions(): Dimensions {
    const { height: screenHeight, width: maxWidth } = this.screenDimensions;
    const maxHeight = screenHeight - 100; // space for close button
    const { dimensions } = this.props.navigation.state.params.media;
    if (dimensions.height < maxHeight && dimensions.width < maxWidth) {
      return dimensions;
    }
    const heightRatio = maxHeight / dimensions.height;
    const widthRatio = maxWidth / dimensions.width;
    if (heightRatio < widthRatio) {
      return {
        height: maxHeight,
        width: dimensions.width * heightRatio,
      };
    } else {
      return {
        width: maxWidth,
        height: dimensions.height * widthRatio,
      };
    }
  }

  get imageContainerStyle() {
    const { height, width } = this.imageDimensions;
    const { height: screenHeight, width: screenWidth } = this.screenDimensions;
    const top = (screenHeight - height) / 2 + contentVerticalOffset;
    const left = (screenWidth - width) / 2;

    const { position } = this.props.transitionProps;
    const { index } = this.props.scene;
    const opacity = position.interpolate({
      inputRange: [ index - 1, index - 0.9 ],
      outputRange: ([ 0, 1 ]: number[]),
      extrapolate: 'clamp',
    });

    const { initialCoordinates } = this.props.navigation.state.params;
    const initialScaleX = initialCoordinates.width / width;
    const scaleX = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialScaleX, 1 ]: number[]),
      extrapolate: 'clamp',
    });
    const initialScaleY = initialCoordinates.height / height;
    const scaleY = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialScaleY, 1 ]: number[]),
      extrapolate: 'clamp',
    });

    const initialTranslateX =
      (initialCoordinates.x + initialCoordinates.width / 2)
      - (left + width / 2);
    const translateX = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialTranslateX, 0 ]: number[]),
      extrapolate: 'clamp',
    });
    const initialTranslateY =
      (initialCoordinates.y + initialCoordinates.height / 2)
      - (top + height / 2);
    const translateY = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialTranslateY, 0 ]: number[]),
      extrapolate: 'clamp',
    });

    const { verticalBounds } = this.props.navigation.state.params;
    return {
      height,
      width,
      marginTop: top - verticalBounds.y,
      marginLeft: left,
      opacity,
      transform: [
        { translateX },
        { translateY },
        { scaleX },
        { scaleY },
      ],
    };
  }

  get contentContainerStyle() {
    const { verticalBounds } = this.props.navigation.state.params;
    const fullScreenHeight = this.screenDimensions.height
      + contentBottomOffset
      + contentVerticalOffset;
    const top = verticalBounds.y;
    const bottom = fullScreenHeight - verticalBounds.y - verticalBounds.height;

    // margin will clip, but padding won't
    const { index } = this.props.scene;
    const verticalStyle = index === this.props.transitionProps.index
      ? { paddingTop: top, paddingBottom: bottom }
      : { marginTop: top, marginBottom: bottom };
    return [ styles.contentContainer, verticalStyle ];
  }

  get backdropStyle() {
    const { position } = this.props.transitionProps;
    const { index } = this.props.scene;
    return {
      opacity: position.interpolate({
        inputRange: [ index - 1, index ],
        outputRange: ([ 0, 1 ]: number[]),
      }),
    };
  }

  render() {
    const { media } = this.props.navigation.state.params;
    return (
      <View style={styles.container}>
        <ConnectedStatusBar barStyle="light-content" />
        <TouchableWithoutFeedback onPress={this.close}>
          <Animated.View style={[ styles.backdrop, this.backdropStyle ]}>
            <TouchableHighlight onPress={this.close} underlayColor="#A0A0A0DD">
              <Icon name="x-circle" style={styles.closeButton} />
            </TouchableHighlight>
          </Animated.View>
        </TouchableWithoutFeedback>
        <View style={this.contentContainerStyle}>
          <TouchableWithoutFeedback onPress={this.close}>
            <View style={styles.cover} />
          </TouchableWithoutFeedback>
          <Animated.View style={this.imageContainerStyle}>
            <Multimedia media={media} spinnerColor="white" />
          </Animated.View>
        </View>
      </View>
    );
  }

  close = () => {
    this.props.navigation.goBack();
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  cover: {
    position: "absolute",
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  },
  backdrop: {
    position: "absolute",
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: "black",
  },
  closeButton: {
    fontSize: 36,
    color: "white",
    position: "absolute",
    top: Platform.OS === "ios" ? contentVerticalOffset : 6,
    right: 12,
  },
  contentContainer: {
    flex: 1,
    overflow: "hidden",
  },
});

export default connect(
  (state: AppState) => ({
    screenDimensions: dimensionsSelector(state),
  }),
)(MultimediaModal);
<commit_msg>[native] Make MultimediaModal close button actually usable<commit_after>// @flow

import type {
  NavigationScreenProp,
  NavigationLeafRoute,
  NavigationScene,
  NavigationTransitionProps,
} from 'react-navigation';
import {
  type Media,
  mediaPropType,
  type Dimensions,
  dimensionsPropType,
} from 'lib/types/media-types';
import type { AppState } from '../redux-setup';
import { type VerticalBounds, verticalBoundsPropType } from './vertical-bounds';

import * as React from 'react';
import {
  View,
  StyleSheet,
  TouchableWithoutFeedback,
  Animated,
  TouchableOpacity,
  Platform,
} from 'react-native';
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/Feather';

import { connect } from 'lib/utils/redux-utils';

import {
  contentVerticalOffset,
  contentBottomOffset,
  dimensionsSelector,
} from '../selectors/dimension-selectors';
import Multimedia from './multimedia.react';
import ConnectedStatusBar from '../connected-status-bar.react';

type LayoutCoordinates = $ReadOnly<{|
  x: number,
  y: number,
  width: number,
  height: number,
|}>;
type NavProp = NavigationScreenProp<{|
  ...NavigationLeafRoute,
  params: {|
    media: Media,
    initialCoordinates: LayoutCoordinates,
    verticalBounds: VerticalBounds,
  |},
|}>;

type Props = {|
  navigation: NavProp,
  scene: NavigationScene,
  transitionProps: NavigationTransitionProps,
  // Redux state
  screenDimensions: Dimensions,
|};
class MultimediaModal extends React.PureComponent<Props> {

  static propTypes = {
    navigation: PropTypes.shape({
      state: PropTypes.shape({
        params: PropTypes.shape({
          media: mediaPropType.isRequired,
          initialCoordinates: PropTypes.shape({
            x: PropTypes.number.isRequired,
            y: PropTypes.number.isRequired,
            width: PropTypes.number.isRequired,
            height: PropTypes.number.isRequired,
          }).isRequired,
          verticalBounds: verticalBoundsPropType.isRequired,
        }).isRequired,
      }).isRequired,
      goBack: PropTypes.func.isRequired,
    }).isRequired,
    transitionProps: PropTypes.object.isRequired,
    scene: PropTypes.object.isRequired,
    screenDimensions: dimensionsPropType.isRequired,
  };

  get screenDimensions(): Dimensions {
    const { screenDimensions } = this.props;
    if (contentVerticalOffset === 0) {
      return screenDimensions;
    }
    const { height, width } = screenDimensions;
    return { height: height - contentVerticalOffset, width };
  }

  get imageDimensions(): Dimensions {
    const { height: screenHeight, width: maxWidth } = this.screenDimensions;
    const maxHeight = screenHeight - 100; // space for close button
    const { dimensions } = this.props.navigation.state.params.media;
    if (dimensions.height < maxHeight && dimensions.width < maxWidth) {
      return dimensions;
    }
    const heightRatio = maxHeight / dimensions.height;
    const widthRatio = maxWidth / dimensions.width;
    if (heightRatio < widthRatio) {
      return {
        height: maxHeight,
        width: dimensions.width * heightRatio,
      };
    } else {
      return {
        width: maxWidth,
        height: dimensions.height * widthRatio,
      };
    }
  }

  get imageContainerStyle() {
    const { height, width } = this.imageDimensions;
    const { height: screenHeight, width: screenWidth } = this.screenDimensions;
    const top = (screenHeight - height) / 2 + contentVerticalOffset;
    const left = (screenWidth - width) / 2;

    const { position } = this.props.transitionProps;
    const { index } = this.props.scene;
    const opacity = position.interpolate({
      inputRange: [ index - 1, index - 0.9 ],
      outputRange: ([ 0, 1 ]: number[]),
      extrapolate: 'clamp',
    });

    const { initialCoordinates } = this.props.navigation.state.params;
    const initialScaleX = initialCoordinates.width / width;
    const scaleX = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialScaleX, 1 ]: number[]),
      extrapolate: 'clamp',
    });
    const initialScaleY = initialCoordinates.height / height;
    const scaleY = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialScaleY, 1 ]: number[]),
      extrapolate: 'clamp',
    });

    const initialTranslateX =
      (initialCoordinates.x + initialCoordinates.width / 2)
      - (left + width / 2);
    const translateX = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialTranslateX, 0 ]: number[]),
      extrapolate: 'clamp',
    });
    const initialTranslateY =
      (initialCoordinates.y + initialCoordinates.height / 2)
      - (top + height / 2);
    const translateY = position.interpolate({
      inputRange: [ index - 1, index ],
      outputRange: ([ initialTranslateY, 0 ]: number[]),
      extrapolate: 'clamp',
    });

    const { verticalBounds } = this.props.navigation.state.params;
    return {
      height,
      width,
      marginTop: top - verticalBounds.y,
      marginLeft: left,
      opacity,
      transform: [
        { translateX },
        { translateY },
        { scaleX },
        { scaleY },
      ],
    };
  }

  get contentContainerStyle() {
    const { verticalBounds } = this.props.navigation.state.params;
    const fullScreenHeight = this.screenDimensions.height
      + contentBottomOffset
      + contentVerticalOffset;
    const top = verticalBounds.y;
    const bottom = fullScreenHeight - verticalBounds.y - verticalBounds.height;

    // margin will clip, but padding won't
    const { index } = this.props.scene;
    const verticalStyle = index === this.props.transitionProps.index
      ? { paddingTop: top, paddingBottom: bottom }
      : { marginTop: top, marginBottom: bottom };
    return [ styles.contentContainer, verticalStyle ];
  }

  get backdropStyle() {
    const { position } = this.props.transitionProps;
    const { index } = this.props.scene;
    return {
      opacity: position.interpolate({
        inputRange: [ index - 1, index ],
        outputRange: ([ 0, 1 ]: number[]),
      }),
    };
  }

  render() {
    const { media } = this.props.navigation.state.params;
    return (
      <View style={styles.container}>
        <ConnectedStatusBar barStyle="light-content" />
        <Animated.View style={[ styles.backdrop, this.backdropStyle ]} />
        <View style={this.contentContainerStyle}>
          <TouchableWithoutFeedback onPress={this.close}>
            <View style={styles.cover} />
          </TouchableWithoutFeedback>
          <Animated.View style={this.imageContainerStyle}>
            <Multimedia media={media} spinnerColor="white" />
          </Animated.View>
        </View>
        <Animated.View style={[
          styles.closeButtonContainer,
          this.backdropStyle,
        ]}>
          <TouchableOpacity onPress={this.close}>
            <Icon name="x-circle" style={styles.closeButton} />
          </TouchableOpacity>
        </Animated.View>
      </View>
    );
  }

  close = () => {
    this.props.navigation.goBack();
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  backdrop: {
    position: "absolute",
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: "black",
  },
  contentContainer: {
    flex: 1,
    overflow: "hidden",
  },
  cover: {
    position: "absolute",
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  },
  closeButtonContainer: {
    position: "absolute",
    top: Platform.OS === "ios" ? contentVerticalOffset : 6,
    right: 12,
  },
  closeButton: {
    fontSize: 36,
    color: "white",
  },
});

export default connect(
  (state: AppState) => ({
    screenDimensions: dimensionsSelector(state),
  }),
)(MultimediaModal);
<|endoftext|>