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

/**
 * An observable property which notifies listeners when the value changes.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const Emitter = require( 'AXON/Emitter' );
  const Multilink = require( 'AXON/Multilink' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );
  const units = require( 'AXON/units' );
  const Validator = require( 'AXON/Validator' );

  // variables
  let globalId = 0; // autoincremented for unique IDs

  class Property extends PhetioObject {

    /**
     * @param {*} value - the initial value of the property
     * @param {Object} [options] - options
     */
    constructor( value, options ) {
      options = _.extend( {

        tandem: Tandem.optional, // workaround for https://github.com/phetsims/tandem/issues/50

        // useDeepEquality: true => Use the `equals` method on the values
        // useDeepEquality: false => Use === for equality test
        useDeepEquality: false,

        // {string|null} units for the number, see units.js
        units: null,

        // {boolean} Whether reentrant calls to 'set' are allowed.
        // Use this to detect or prevent update cycles. Update cycles may be due to floating point error,
        // faulty logic, etc. This may be of particular interest for PhET-iO instrumentation, where such
        // cycles may pollute the data stream. See https://github.com/phetsims/axon/issues/179
        reentrant: false

        // See Validator.DEFAULT_OPTIONS for validation options.
      }, Validator.DEFAULT_OPTIONS, {

        // By default, check the options once in the constructor, not on each subsequent value validation, to improve
        // performance in requirejs mode
        validateOptionsOnValidateValue: false
      }, options );

      assert && Validator.validateOptions( options );

      assert && options.units && assert( units.isValidUnits( options.units ), 'invalid units: ' + options.units );
      if ( options.units ) {
        options.phetioEventMetadata = options.phetioEventMetadata || {};
        assert && assert( !options.phetioEventMetadata.hasOwnProperty( 'units' ), 'units should be supplied by Property, not elsewhere' );
        options.phetioEventMetadata.units = options.units;
      }

      super( options );

      // @public {number} - Unique identifier for this Property.
      this.id = globalId++;

      // @public (phet-io) Units, if any.  See units.js for valid values
      this.units = options.units;

      // @private (read-only) whether to use the values' equals method or === equality
      // useDeepEquality: true => Use the `equals` method on the values
      // useDeepEquality: false => Use === for equality test
      this.useDeepEquality = options.useDeepEquality;

      // @private {function|false} - closure over options for validation in set()
      this.validate = assert && ( value => Validator.validate( value, options ) );

      // validate the initial value
      assert && this.validate( value );

      // When running as phet-io, if the tandem is specified, the type must be specified.
      // This assertion helps in instrumenting code that has the tandem but not type
      Tandem.validationEnabled() && this.isPhetioInstrumented() && assert && assert( !!options.phetioType,
        'phetioType passed to Property must be specified. Tandem.phetioID: ' + this.tandem.phetioID );

      // When running as phet-io, if the tandem is specified, the type must be specified.
      // This assertion helps in instrumenting code that has the tandem but not type
      Tandem.validationEnabled() && this.isPhetioInstrumented() && assert && assert( !!options.phetioType.elementType,
        'phetioType.elementType must be specified. Tandem.phetioID: ' + this.tandem.phetioID );

      // @private - Store the internal value and the initial value
      this._value = value;

      // @protected - Initial value
      this._initialValue = value;

      // @public (phet-io)
      this.validValues = options.validValues;

      // @private (unit-tests) - emit1 is called when the value changes (or on link)
      // Also used in ShapePlacementBoard.js at the moment
      this.changedEmitter = new Emitter();

      // @private whether we are in the process of notifying listeners
      this.notifying = false;

      // @private whether to allow reentry of calls to set
      this.reentrant = options.reentrant;

      // @private - while deferred, new values neither take effect nor send notifications.  When isDeferred changes from
      // true to false, the final deferred value becomes the Property value.  An action is created which can be invoked to
      // send notifications.
      this.isDeferred = false;

      // @private {*} - the value that this Property will take after no longer deferred
      this.deferredValue = null;

      // @private {boolean} whether a deferred value has been set
      this.hasDeferredValue = false;
    }

    /**
     * Gets the value.
     * You can also use the es5 getter (property.value) but this means is provided for inner loops
     * or internal code that must be fast.
     * @returns {*}
     * @public
     */
    get() {
      return this._value;
    }

    /**
     * Sets the value and notifies listeners, unless deferred. You can also use the es5 getter (property.value) but
     * this means is provided for inner loops or internal code that must be fast. If the value hasn't changed, this is
     * a no-op.
     *
     * @param {*} value
     * @returns {Property} this instance, for chaining.
     * @public
     */
    set( value ) {
      this.validate && this.validate( value );
      if ( this.isDeferred ) {
        this.deferredValue = value;
        this.hasDeferredValue = true;
      }
      else if ( !this.equalsValue( value ) ) {
        this.setValueAndNotifyListeners( value );
      }
      return this;
    }

    /**
     * Returns true if and only if the specified value equals the value of this property
     * @param {Object} value
     * @returns {boolean}
     * @protected
     */
    equalsValue( value ) {
      return this.areValuesEqual( value, this._value );
    }

    /**
     * Determines equality semantics for the wrapped type, including whether notifications are sent out when the
     * wrapped value changes, and whether onValue is triggered.
     *
     * useDeepEquality: true => Use the `equals` method on the values
     * useDeepEquality: false => Use === for equality test
     *
     * Alternatively different implementation can be provided by subclasses or instances to change the equals
     * definition. See #10 and #73 and #115
     * @param {Object} a - should have the same type as Property element type
     * @param {Object} b - should have the same type as Property element type
     * @returns {boolean}
     * @private
     */
    areValuesEqual( a, b ) {
      if ( this.useDeepEquality && a && b && a.constructor === b.constructor ) {

        assert && assert( !!a.equals, 'no equals function for 1st arg' );
        assert && assert( !!b.equals, 'no equals function for 2nd arg' );
        assert && assert( a.equals( b ) === b.equals( a ), 'incompatible equality checks' );
        return a.equals( b );
      }
      else {

        // Reference equality for objects, value equality for primitives
        return a === b;
      }
    }

    /**
     * Returns the initial value of this Property.
     * @public
     *
     * @returns {*}
     */
    getInitialValue() {
      return this._initialValue;
    }

    // @public
    get initialValue() {
      return this.getInitialValue();
    }

    /**
     * Updates the value of this node
     * @param {*} value - the new value this Property will take, which is different than the previous value.
     * @protected - can be overridden.
     */
    setValueAndNotifyListeners( value ) {
      const oldValue = this.get();
      this._value = value;
      this._notifyListeners( oldValue );
    }

    /**
     * @param {*} oldValue
     * @private
     */
    _notifyListeners( oldValue ) {

      this.phetioStartEvent( 'changed', () => {
        return {
          oldValue: this.phetioType.elementType.toStateObject( oldValue ),
          newValue: this.phetioType.elementType.toStateObject( this.get() )
        };
      } );

      // notify listeners, optionally detect loops where this Property is set again before this completes.
      assert && assert( !this.notifying || this.reentrant,
        'reentry detected, value=' + this.get() + ', oldValue=' + oldValue );
      this.notifying = true;
      this.changedEmitter.emit3( this.get(), oldValue, this );
      this.notifying = false;

      this.isPhetioInstrumented() && this.phetioEndEvent();
    }

    /**
     * Use this method when mutating a value (not replacing with a new instance) and you want to send notifications about the change.
     * This is different from the normal axon strategy, but may be necessary to prevent memory allocations.
     * This method is unsafe for removing listeners because it assumes the listener list not modified, to save another allocation
     * Only provides the new reference as a callback (no oldvalue)
     * See https://github.com/phetsims/axon/issues/6
     * @public
     */
    notifyListenersStatic() {
      this.changedEmitter.emit3( this.get(), undefined, this );
    }

    /**
     * When deferred, set values do not take effect or send out notifications.  After defer ends, the Property takes
     * its deferred value (if any), and a follow-up action (return value) can be invoked to send out notifications
     * once other Properties have also taken their deferred values.
     *
     * @param {boolean} isDeferred - whether the Property should be deferred or not
     * @returns {function|null} - action that can be used to send notifications after final setDeferred(false),
     *                          - if the value changed
     * @public
     */
    setDeferred( isDeferred ) {
      assert && assert( isDeferred || !isDeferred, 'bad value for isDeferred' );
      if ( isDeferred ) {
        assert && assert( !this.isDeferred, 'Property already deferred' );
        this.isDeferred = true;
      }
      else if ( !isDeferred ) {
        assert && assert( this.isDeferred, 'Property wasn\'t deferred' );
        this.isDeferred = false;

        const oldValue = this._value;

        // Take the new value
        if ( this.hasDeferredValue ) {
          this._value = this.deferredValue;
          this.hasDeferredValue = false;
        }

        // If the value has changed, prepare to send out notifications (after all other Properties in this transaction
        // have their final values)
        if ( !this.equalsValue( oldValue ) ) {
          return () => this._notifyListeners( oldValue );
        }
      }

      // no action to signify change
      return null;
    }

    /**
     * Resets the value to the initial value.
     * @public
     */
    reset() {
      this.set( this._initialValue );
    }

    // @public
    get value() { return this.get(); }

    // @public
    set value( newValue ) { this.set( newValue ); }

    /**
     * Adds listener and calls it immediately. If listener is already registered, this is a no-op. The initial
     * notification provides the current value for newValue and null for oldValue.
     *
     * @param {function} listener a function of the form listener(newValue,oldValue)
     * @public
     */
    link( listener ) {
      this.changedEmitter.addListener( listener );
      listener( this.get(), null, this ); // null should be used when an object is expected but unavailable
    }

    /**
     * Add an listener to the Property, without calling it back right away.
     * This is used when you need to register a listener without an immediate callback.
     *
     * @param {function} listener - a function with a single argument, which is the value of the property at the time the function is called.
     * @public
     */
    lazyLink( listener ) {
      this.changedEmitter.addListener( listener );
    }

    /**
     * Removes a listener. If listener is not registered, this is a no-op.
     *
     * @param {function} listener
     * @public
     */
    unlink( listener ) {
      this.changedEmitter.removeListener( listener );
    }

    /**
     * Removes all listeners. If no listeners are registered, this is a no-op.
     * @public
     */
    unlinkAll() {
      this.changedEmitter.removeAllListeners();
    }

    /**
     * Links an object's named attribute to this property.  Returns a handle so it can be removed using Property.unlink();
     * Example: modelVisibleProperty.linkAttribute(view,'visible');
     *
     * @param object
     * @param attributeName
     * @public
     */
    linkAttribute( object, attributeName ) {
      const handle = value => {object[ attributeName ] = value;};
      this.link( handle );
      return handle;
    }

    /**
     * Unlink an listener added with linkAttribute.  Note: the args of linkAttribute do not match the args of
     * unlinkAttribute: here, you must pass the listener handle returned by linkAttribute rather than object and attributeName
     *
     * @param {function} listener
     * @public
     */
    unlinkAttribute( listener ) {
      this.unlink( listener );
    }

    // @public Provide toString for console debugging, see http://stackoverflow.com/questions/2485632/valueof-vs-tostring-in-javascript
    toString() {return 'Property#' + this.id + '{' + this.get() + '}'; }

    // @public
    valueOf() {return this.toString();}

    /**
     * Convenience function for debugging a property values.  It prints the new value on registration and when changed.
     * @param name debug name to be printed on the console
     * @returns {function} the handle to the linked listener in case it needs to be removed later
     * @public
     */
    debug( name ) {
      const listener = value => console.log( name, value );
      this.link( listener );
      return listener;
    }

    /**
     * Modifies the value of this Property with the ! operator.  Works for booleans and non-booleans.
     * @public
     */
    toggle() {
      this.value = !this.value;
    }

    // @public Ensures that the Property is eligible for GC
    dispose() {

      // remove any listeners that are still attached to this property
      this.unlinkAll();

      super.dispose();
      this.changedEmitter.dispose();
    }

    /**
     * Checks whether a listener is registered with this Property
     * @param {function} listener
     * @returns {boolean}
     * @public
     */
    hasListener( listener ) {
      return this.changedEmitter.hasListener( listener );
    }

    /**
     * Returns true if there are any listeners.
     * @returns {boolean}
     * @public
     */
    hasListeners() {
      assert && assert( arguments.length === 0, 'Property.hasListeners should be called without arguments' );
      return this.changedEmitter.hasListeners();
    }

    /**
     * Registers a listener with multiple properties, then notifies the listener immediately.
     * @param {Property[]} properties
     * @param {function} listener function that takes values from the properties and returns nothing
     * @returns {Multilink}
     * @static
     */
    static multilink( properties, listener ) {
      return new Multilink( properties, listener, false );
    }

    /**
     * Registers an listener with multiple properties *without* an immediate callback with current values.
     * @param {Property[]} properties
     * @param {function} listener function that takes values from the properties and returns nothing
     * @returns {Multilink}
     * @static
     */
    static lazyMultilink( properties, listener ) {
      return new Multilink( properties, listener, true );
    }

    /**
     * Unlinks an listener that was added with multilink or lazyMultilink.
     * @param {Multilink} multilink
     * @static
     */
    static unmultilink() {
      multilink => multilink.dispose();
    }
  }

  return axon.register( 'Property', Property );
} );<commit_msg>Add back missing parameter, see https://github.com/phetsims/axon/issues/213<commit_after>// Copyright 2013-2018, University of Colorado Boulder

/**
 * An observable property which notifies listeners when the value changes.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const Emitter = require( 'AXON/Emitter' );
  const Multilink = require( 'AXON/Multilink' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );
  const units = require( 'AXON/units' );
  const Validator = require( 'AXON/Validator' );

  // variables
  let globalId = 0; // autoincremented for unique IDs

  class Property extends PhetioObject {

    /**
     * @param {*} value - the initial value of the property
     * @param {Object} [options] - options
     */
    constructor( value, options ) {
      options = _.extend( {

        tandem: Tandem.optional, // workaround for https://github.com/phetsims/tandem/issues/50

        // useDeepEquality: true => Use the `equals` method on the values
        // useDeepEquality: false => Use === for equality test
        useDeepEquality: false,

        // {string|null} units for the number, see units.js
        units: null,

        // {boolean} Whether reentrant calls to 'set' are allowed.
        // Use this to detect or prevent update cycles. Update cycles may be due to floating point error,
        // faulty logic, etc. This may be of particular interest for PhET-iO instrumentation, where such
        // cycles may pollute the data stream. See https://github.com/phetsims/axon/issues/179
        reentrant: false

        // See Validator.DEFAULT_OPTIONS for validation options.
      }, Validator.DEFAULT_OPTIONS, {

        // By default, check the options once in the constructor, not on each subsequent value validation, to improve
        // performance in requirejs mode
        validateOptionsOnValidateValue: false
      }, options );

      assert && Validator.validateOptions( options );

      assert && options.units && assert( units.isValidUnits( options.units ), 'invalid units: ' + options.units );
      if ( options.units ) {
        options.phetioEventMetadata = options.phetioEventMetadata || {};
        assert && assert( !options.phetioEventMetadata.hasOwnProperty( 'units' ), 'units should be supplied by Property, not elsewhere' );
        options.phetioEventMetadata.units = options.units;
      }

      super( options );

      // @public {number} - Unique identifier for this Property.
      this.id = globalId++;

      // @public (phet-io) Units, if any.  See units.js for valid values
      this.units = options.units;

      // @private (read-only) whether to use the values' equals method or === equality
      // useDeepEquality: true => Use the `equals` method on the values
      // useDeepEquality: false => Use === for equality test
      this.useDeepEquality = options.useDeepEquality;

      // @private {function|false} - closure over options for validation in set()
      this.validate = assert && ( value => Validator.validate( value, options ) );

      // validate the initial value
      assert && this.validate( value );

      // When running as phet-io, if the tandem is specified, the type must be specified.
      // This assertion helps in instrumenting code that has the tandem but not type
      Tandem.validationEnabled() && this.isPhetioInstrumented() && assert && assert( !!options.phetioType,
        'phetioType passed to Property must be specified. Tandem.phetioID: ' + this.tandem.phetioID );

      // When running as phet-io, if the tandem is specified, the type must be specified.
      // This assertion helps in instrumenting code that has the tandem but not type
      Tandem.validationEnabled() && this.isPhetioInstrumented() && assert && assert( !!options.phetioType.elementType,
        'phetioType.elementType must be specified. Tandem.phetioID: ' + this.tandem.phetioID );

      // @private - Store the internal value and the initial value
      this._value = value;

      // @protected - Initial value
      this._initialValue = value;

      // @public (phet-io)
      this.validValues = options.validValues;

      // @private (unit-tests) - emit1 is called when the value changes (or on link)
      // Also used in ShapePlacementBoard.js at the moment
      this.changedEmitter = new Emitter();

      // @private whether we are in the process of notifying listeners
      this.notifying = false;

      // @private whether to allow reentry of calls to set
      this.reentrant = options.reentrant;

      // @private - while deferred, new values neither take effect nor send notifications.  When isDeferred changes from
      // true to false, the final deferred value becomes the Property value.  An action is created which can be invoked to
      // send notifications.
      this.isDeferred = false;

      // @private {*} - the value that this Property will take after no longer deferred
      this.deferredValue = null;

      // @private {boolean} whether a deferred value has been set
      this.hasDeferredValue = false;
    }

    /**
     * Gets the value.
     * You can also use the es5 getter (property.value) but this means is provided for inner loops
     * or internal code that must be fast.
     * @returns {*}
     * @public
     */
    get() {
      return this._value;
    }

    /**
     * Sets the value and notifies listeners, unless deferred. You can also use the es5 getter (property.value) but
     * this means is provided for inner loops or internal code that must be fast. If the value hasn't changed, this is
     * a no-op.
     *
     * @param {*} value
     * @returns {Property} this instance, for chaining.
     * @public
     */
    set( value ) {
      this.validate && this.validate( value );
      if ( this.isDeferred ) {
        this.deferredValue = value;
        this.hasDeferredValue = true;
      }
      else if ( !this.equalsValue( value ) ) {
        this.setValueAndNotifyListeners( value );
      }
      return this;
    }

    /**
     * Returns true if and only if the specified value equals the value of this property
     * @param {Object} value
     * @returns {boolean}
     * @protected
     */
    equalsValue( value ) {
      return this.areValuesEqual( value, this._value );
    }

    /**
     * Determines equality semantics for the wrapped type, including whether notifications are sent out when the
     * wrapped value changes, and whether onValue is triggered.
     *
     * useDeepEquality: true => Use the `equals` method on the values
     * useDeepEquality: false => Use === for equality test
     *
     * Alternatively different implementation can be provided by subclasses or instances to change the equals
     * definition. See #10 and #73 and #115
     * @param {Object} a - should have the same type as Property element type
     * @param {Object} b - should have the same type as Property element type
     * @returns {boolean}
     * @private
     */
    areValuesEqual( a, b ) {
      if ( this.useDeepEquality && a && b && a.constructor === b.constructor ) {

        assert && assert( !!a.equals, 'no equals function for 1st arg' );
        assert && assert( !!b.equals, 'no equals function for 2nd arg' );
        assert && assert( a.equals( b ) === b.equals( a ), 'incompatible equality checks' );
        return a.equals( b );
      }
      else {

        // Reference equality for objects, value equality for primitives
        return a === b;
      }
    }

    /**
     * Returns the initial value of this Property.
     * @public
     *
     * @returns {*}
     */
    getInitialValue() {
      return this._initialValue;
    }

    // @public
    get initialValue() {
      return this.getInitialValue();
    }

    /**
     * Updates the value of this node
     * @param {*} value - the new value this Property will take, which is different than the previous value.
     * @protected - can be overridden.
     */
    setValueAndNotifyListeners( value ) {
      const oldValue = this.get();
      this._value = value;
      this._notifyListeners( oldValue );
    }

    /**
     * @param {*} oldValue
     * @private
     */
    _notifyListeners( oldValue ) {

      this.phetioStartEvent( 'changed', () => {
        return {
          oldValue: this.phetioType.elementType.toStateObject( oldValue ),
          newValue: this.phetioType.elementType.toStateObject( this.get() )
        };
      } );

      // notify listeners, optionally detect loops where this Property is set again before this completes.
      assert && assert( !this.notifying || this.reentrant,
        'reentry detected, value=' + this.get() + ', oldValue=' + oldValue );
      this.notifying = true;
      this.changedEmitter.emit3( this.get(), oldValue, this );
      this.notifying = false;

      this.isPhetioInstrumented() && this.phetioEndEvent();
    }

    /**
     * Use this method when mutating a value (not replacing with a new instance) and you want to send notifications about the change.
     * This is different from the normal axon strategy, but may be necessary to prevent memory allocations.
     * This method is unsafe for removing listeners because it assumes the listener list not modified, to save another allocation
     * Only provides the new reference as a callback (no oldvalue)
     * See https://github.com/phetsims/axon/issues/6
     * @public
     */
    notifyListenersStatic() {
      this.changedEmitter.emit3( this.get(), undefined, this );
    }

    /**
     * When deferred, set values do not take effect or send out notifications.  After defer ends, the Property takes
     * its deferred value (if any), and a follow-up action (return value) can be invoked to send out notifications
     * once other Properties have also taken their deferred values.
     *
     * @param {boolean} isDeferred - whether the Property should be deferred or not
     * @returns {function|null} - action that can be used to send notifications after final setDeferred(false),
     *                          - if the value changed
     * @public
     */
    setDeferred( isDeferred ) {
      assert && assert( isDeferred || !isDeferred, 'bad value for isDeferred' );
      if ( isDeferred ) {
        assert && assert( !this.isDeferred, 'Property already deferred' );
        this.isDeferred = true;
      }
      else if ( !isDeferred ) {
        assert && assert( this.isDeferred, 'Property wasn\'t deferred' );
        this.isDeferred = false;

        const oldValue = this._value;

        // Take the new value
        if ( this.hasDeferredValue ) {
          this._value = this.deferredValue;
          this.hasDeferredValue = false;
        }

        // If the value has changed, prepare to send out notifications (after all other Properties in this transaction
        // have their final values)
        if ( !this.equalsValue( oldValue ) ) {
          return () => this._notifyListeners( oldValue );
        }
      }

      // no action to signify change
      return null;
    }

    /**
     * Resets the value to the initial value.
     * @public
     */
    reset() {
      this.set( this._initialValue );
    }

    // @public
    get value() { return this.get(); }

    // @public
    set value( newValue ) { this.set( newValue ); }

    /**
     * Adds listener and calls it immediately. If listener is already registered, this is a no-op. The initial
     * notification provides the current value for newValue and null for oldValue.
     *
     * @param {function} listener a function of the form listener(newValue,oldValue)
     * @public
     */
    link( listener ) {
      this.changedEmitter.addListener( listener );
      listener( this.get(), null, this ); // null should be used when an object is expected but unavailable
    }

    /**
     * Add an listener to the Property, without calling it back right away.
     * This is used when you need to register a listener without an immediate callback.
     *
     * @param {function} listener - a function with a single argument, which is the value of the property at the time the function is called.
     * @public
     */
    lazyLink( listener ) {
      this.changedEmitter.addListener( listener );
    }

    /**
     * Removes a listener. If listener is not registered, this is a no-op.
     *
     * @param {function} listener
     * @public
     */
    unlink( listener ) {
      this.changedEmitter.removeListener( listener );
    }

    /**
     * Removes all listeners. If no listeners are registered, this is a no-op.
     * @public
     */
    unlinkAll() {
      this.changedEmitter.removeAllListeners();
    }

    /**
     * Links an object's named attribute to this property.  Returns a handle so it can be removed using Property.unlink();
     * Example: modelVisibleProperty.linkAttribute(view,'visible');
     *
     * @param object
     * @param attributeName
     * @public
     */
    linkAttribute( object, attributeName ) {
      const handle = value => {object[ attributeName ] = value;};
      this.link( handle );
      return handle;
    }

    /**
     * Unlink an listener added with linkAttribute.  Note: the args of linkAttribute do not match the args of
     * unlinkAttribute: here, you must pass the listener handle returned by linkAttribute rather than object and attributeName
     *
     * @param {function} listener
     * @public
     */
    unlinkAttribute( listener ) {
      this.unlink( listener );
    }

    // @public Provide toString for console debugging, see http://stackoverflow.com/questions/2485632/valueof-vs-tostring-in-javascript
    toString() {return 'Property#' + this.id + '{' + this.get() + '}'; }

    // @public
    valueOf() {return this.toString();}

    /**
     * Convenience function for debugging a property values.  It prints the new value on registration and when changed.
     * @param name debug name to be printed on the console
     * @returns {function} the handle to the linked listener in case it needs to be removed later
     * @public
     */
    debug( name ) {
      const listener = value => console.log( name, value );
      this.link( listener );
      return listener;
    }

    /**
     * Modifies the value of this Property with the ! operator.  Works for booleans and non-booleans.
     * @public
     */
    toggle() {
      this.value = !this.value;
    }

    // @public Ensures that the Property is eligible for GC
    dispose() {

      // remove any listeners that are still attached to this property
      this.unlinkAll();

      super.dispose();
      this.changedEmitter.dispose();
    }

    /**
     * Checks whether a listener is registered with this Property
     * @param {function} listener
     * @returns {boolean}
     * @public
     */
    hasListener( listener ) {
      return this.changedEmitter.hasListener( listener );
    }

    /**
     * Returns true if there are any listeners.
     * @returns {boolean}
     * @public
     */
    hasListeners() {
      assert && assert( arguments.length === 0, 'Property.hasListeners should be called without arguments' );
      return this.changedEmitter.hasListeners();
    }

    /**
     * Registers a listener with multiple properties, then notifies the listener immediately.
     * @param {Property[]} properties
     * @param {function} listener function that takes values from the properties and returns nothing
     * @returns {Multilink}
     * @static
     */
    static multilink( properties, listener ) {
      return new Multilink( properties, listener, false );
    }

    /**
     * Registers an listener with multiple properties *without* an immediate callback with current values.
     * @param {Property[]} properties
     * @param {function} listener function that takes values from the properties and returns nothing
     * @returns {Multilink}
     * @static
     */
    static lazyMultilink( properties, listener ) {
      return new Multilink( properties, listener, true );
    }

    /**
     * Unlinks an listener that was added with multilink or lazyMultilink.
     * @param {Multilink} multilink
     * @static
     */
    static unmultilink( multilink ) {
      multilink => multilink.dispose();
    }
  }

  return axon.register( 'Property', Property );
} );<|endoftext|>


<commit_before>function Dungeon(scene, player) {
	var self = this;
	this.objects = [];
	var dummy_material = new THREE.MeshBasicMaterial({color: 0x000000});
	var debug_material = new THREE.MeshBasicMaterial({color: 0xff00ff});

	var WALL = "#";
	var OPEN = ".";

	this.generateMesh = function(level) {
		var block_materials = [
			cache.getMaterial(level.materials.wall), // right
			cache.getMaterial(level.materials.wall), // left
			dummy_material, // top
			dummy_material, // bottom
			cache.getMaterial(level.materials.wall), // back
			cache.getMaterial(level.materials.wall)  // front
		];
		var block_params = {};
		if (assets.materials[level.materials.wall] && assets.materials[level.materials.wall].roughness)
			block_params.roughness = assets.materials[level.materials.wall].roughness;

		// Level geometry
		var geometry = new THREE.Geometry(), mesh;
		var cell, px, nx, pz, nz, tess, cube;
		for (var j = 0; j < level.depth; ++j) {
			for (var i = 0; i < level.width; ++i) {
				px = nx = pz = nz = py = ny = 0;
				cell = level.get(i, j);
				if (cell === OPEN) continue;
				if (cell === WALL) {
					px = level.get(i + 1, j) == WALL ? 0 : 1;
					nx = level.get(i - 1, j) == WALL ? 0 : 2;
					pz = level.get(i, j + 1) == WALL ? 0 : 4;
					nz = level.get(i, j - 1) == WALL ? 0 : 8;
					// If wall completely surrounded by walls, skip
					if (px + nx + pz + nz === 0) continue;
					tess = block_params.roughness ? 10 : 0;
					cube = new BlockGeometry(level.gridSize, level.roomHeight, level.gridSize,
						tess, tess, tess, block_materials,
						{ px: px, nx: nx, py: 0, ny: 0, pz: pz, nz: nz },
						level.gridSize/2, level.roomHeight/2, block_params.roughness);
					mesh = new THREE.Mesh(cube);
					mesh.position.x = (i + 0.5) * level.gridSize;
					mesh.position.y = 0.5 * level.roomHeight;
					mesh.position.z = (j + 0.5) * level.gridSize;
					THREE.GeometryUtils.merge(geometry, mesh);
					// Collision body
					// Bounding box needs tweaking if there is only one side in the block
					cube.computeBoundingBox();
					if (Math.abs(cube.boundingBox.max.x - cube.boundingBox.min.x) <= 0.5) {
						cube.boundingBox.min.x = -0.5 * level.gridSize;
						cube.boundingBox.max.x = 0.5 * level.gridSize;
					}
					if (Math.abs(cube.boundingBox.max.z - cube.boundingBox.min.z) <= 0.5) {
						cube.boundingBox.min.z = -0.5 * level.gridSize;
						cube.boundingBox.max.z = 0.5 * level.gridSize;
					}
					var wallbody = new Physijs.BoxMesh(cube, dummy_material, 0);
					wallbody.position.copy(mesh.position);
					wallbody.visible = false;
					scene.add(wallbody);
				} else {

				}
			}
		}

		// Ceiling, no collision needed
		var ceiling_plane = new THREE.Mesh(
			new PlaneGeometry(level.gridSize * level.width, level.gridSize * level.depth,
				1, 1, "ny", level.width, level.depth),
			cache.getMaterial(level.materials.ceiling)
		);
		ceiling_plane.position.set(level.gridSize * level.width * 0.5, level.roomHeight, level.gridSize * level.depth * 0.5);
		scene.add(ceiling_plane);

		// Floor with collision
		var floor_plane = new Physijs.PlaneMesh(
			new PlaneGeometry(level.gridSize * level.width, level.gridSize * level.depth,
				1, 1, "py", level.width, level.depth),
			Physijs.createMaterial(cache.getMaterial(level.materials.floor), 0.9, 0.0), // friction, restitution
			0 // mass
		);
		floor_plane.position.set(level.gridSize * level.width * 0.5, 0.0, level.gridSize * level.depth * 0.5);
		floor_plane.receiveShadow = true;
		scene.add(floor_plane);

		// Level mesh
		geometry.computeTangents();
		mesh = new THREE.Mesh(geometry, new THREE.MeshFaceMaterial());
		mesh.receiveShadow = true;
		scene.add(mesh);
	};

	this.addLights = function(level) {
		// Ambient
		scene.add(new THREE.AmbientLight(0xaaaaaa));

		// Point lights
		for (var i = 0; i < level.lights.length; ++i) {
			// Actual light
			var light = new THREE.PointLight(0xffffaa, 1, 2 * level.gridSize);
			light.position.copy(level.lights[i].position);
			light.position.x *= level.gridSize;
			light.position.z *= level.gridSize;
			scene.add(light);
			lightManager.addLight(light);

			// Shadow casting light
			if (level.lights[i].target) {
				var light2 = new THREE.SpotLight(0xffffaa, light.intensity, light.distance);
				light2.position = light.position;
				light2.target.position.copy(level.lights[i].target);
				light2.angle = Math.PI / 2;
				light2.castShadow = true;
				light2.onlyShadow = true;
				light2.shadowCameraNear = 0.1 * UNIT;
				light2.shadowCameraFar = light.distance * 1.5 * UNIT;
				light2.shadowCameraFov = 100;
				light2.shadowBias = -0.0002;
				light2.shadowDarkness = 0.3;
				light2.shadowMapWidth = 256;
				light2.shadowMapHeight = 256;
				light2.shadowCameraVisible = false;
				scene.add(light2);
				lightManager.addShadow(light2);
			}

			// Flame
			if (CONFIG.particles)
				light.emitter = createSimpleFire(light.position);
		}

		// Player's torch
		player.light = new THREE.PointLight(0x88bbff, 1, level.gridSize * 3);
		scene.add(player.light);
		player.shadow = new THREE.SpotLight(player.light.color, player.light.intensity, player.light.distance);
		player.shadow.angle = Math.PI / 4;
		player.shadow.onlyShadow = true;
		player.shadow.castShadow = true;
		player.shadow.shadowCameraNear = 0.1 * UNIT;
		player.shadow.shadowCameraFar = 10 * UNIT;
		player.shadow.shadowCameraFov = 90;
		player.shadow.shadowBias = -0.0002;
		player.shadow.shadowDarkness = 0.3;
		player.shadow.shadowMapWidth = 1024;
		player.shadow.shadowMapHeight = 1024;
		player.shadow.shadowCameraVisible = false;
		scene.add(player.shadow);
	};

	this.addObjects = function(level) {
		function objectHandler(pos, def) {
			return function handleObject(geometry) {
				if (!def) def = {};
				var obj, mass = def.mass || 0;
				var scale = 1.0;
				if (def.randScale) {
					scale += randf(-def.randScale, def.randScale);
					mass *= scale;
				}
				for (var m = 0; m < geometry.materials.length; ++m)
					fixAnisotropy(geometry.materials[m]);
				var mat = geometry.materials.length > 1 ? new THREE.MeshFaceMaterial() : geometry.materials[0];
				if (def.collision) {
					var material = Physijs.createMaterial(mat, 0.7, 0.2); // friction, restition
					if (def.collision == "plane")
						obj = new Physijs.PlaneMesh(geometry, material, mass);
					else if (def.collision == "box")
						obj = new Physijs.BoxMesh(geometry, material, mass);
					else if (def.collision == "sphere")
						obj = new Physijs.SphereMesh(geometry, material, mass);
					else if (def.collision == "cylinder")
						obj = new Physijs.CylinderMesh(geometry, material, mass);
					else if (def.collision == "cone")
						obj = new Physijs.ConeMesh(geometry, material, mass);
					else if (def.collision == "convex")
						obj = new Physijs.ConvexMesh(geometry, material, mass);
					else throw "Unsupported collision mesh type " + def.collision;
					self.objects.push(obj);
				} else {
					obj = new THREE.Mesh(geometry, mat);
				}
				pos.x *= level.gridSize;
				pos.z *= level.gridSize;
				// Auto-height
				if (pos.y === null) {
					if (!geometry.boundingBox) geometry.computeBoundingBox();
					pos.y = 0.5 * (geometry.boundingBox.max.y - geometry.boundingBox.min.y) + 0.001;
				}
				obj.position.copy(pos);
				obj.scale.set(scale, scale, scale);
				if (!def.noShadows) {
					obj.castShadow = true;
					obj.receiveShadow = true;
				}
				scene.add(obj);
			};
		}

		for (var i = 0; i < level.objects.length; ++i) {
			var objname = level.objects[i].name;
			var obj = cache.loadModel("assets/models/" + objname + "/" + objname + ".js",
				objectHandler(new THREE.Vector3().copy(level.objects[i].position), assets.objects[objname]));
		}
	};

	if (queryParams.level) {
		if (queryParams.level == "rand") {
			var gen = new MapGen();
			this.level = gen.generate();
		} else if (queryParams.level.length > 24) {
			var json = window.atob(queryParams.level);
			this.level = JSON.parse(json);
		} else {
			// TODO: Load the level named in the parameter
		}
	} else this.level = testLevel;

	this.level.get = this.level.get || function(x, z) {
		if (x < 0 || x >= this.width) return WALL;
		else if (z < 0 || z >= this.depth) return WALL;
		return this.map[z * this.width + x];
	};

	this.level.set = this.level.set || function(x, z, obj) {
		if (x < 0 || x >= this.width) return;
		else if (z < 0 || z >= this.depth) return;
		this.map[z * this.width + x] = obj;
	};

	// TODO: Set player rotation
	player.geometry.computeBoundingBox();
	player.position.x = this.level.start[0] * this.level.gridSize;
	player.position.y = 0.5 * (player.geometry.boundingBox.max.y - player.geometry.boundingBox.min.y) + 0.001;
	player.position.z = this.level.start[1] * this.level.gridSize;

	this.generateMesh(this.level);
	this.addLights(this.level);
	this.addObjects(this.level);
}
<commit_msg>Place torch meshes together with the lights.<commit_after>function Dungeon(scene, player) {
	var self = this;
	this.objects = [];
	var dummy_material = new THREE.MeshBasicMaterial({color: 0x000000});
	var debug_material = new THREE.MeshBasicMaterial({color: 0xff00ff});

	var WALL = "#";
	var OPEN = ".";

	this.generateMesh = function(level) {
		var block_materials = [
			cache.getMaterial(level.materials.wall), // right
			cache.getMaterial(level.materials.wall), // left
			dummy_material, // top
			dummy_material, // bottom
			cache.getMaterial(level.materials.wall), // back
			cache.getMaterial(level.materials.wall)  // front
		];
		var block_params = {};
		if (assets.materials[level.materials.wall] && assets.materials[level.materials.wall].roughness)
			block_params.roughness = assets.materials[level.materials.wall].roughness;

		// Level geometry
		var geometry = new THREE.Geometry(), mesh;
		var cell, px, nx, pz, nz, tess, cube;
		for (var j = 0; j < level.depth; ++j) {
			for (var i = 0; i < level.width; ++i) {
				px = nx = pz = nz = py = ny = 0;
				cell = level.get(i, j);
				if (cell === OPEN) continue;
				if (cell === WALL) {
					px = level.get(i + 1, j) == WALL ? 0 : 1;
					nx = level.get(i - 1, j) == WALL ? 0 : 2;
					pz = level.get(i, j + 1) == WALL ? 0 : 4;
					nz = level.get(i, j - 1) == WALL ? 0 : 8;
					// If wall completely surrounded by walls, skip
					if (px + nx + pz + nz === 0) continue;
					tess = block_params.roughness ? 10 : 0;
					cube = new BlockGeometry(level.gridSize, level.roomHeight, level.gridSize,
						tess, tess, tess, block_materials,
						{ px: px, nx: nx, py: 0, ny: 0, pz: pz, nz: nz },
						level.gridSize/2, level.roomHeight/2, block_params.roughness);
					mesh = new THREE.Mesh(cube);
					mesh.position.x = (i + 0.5) * level.gridSize;
					mesh.position.y = 0.5 * level.roomHeight;
					mesh.position.z = (j + 0.5) * level.gridSize;
					THREE.GeometryUtils.merge(geometry, mesh);
					// Collision body
					// Bounding box needs tweaking if there is only one side in the block
					cube.computeBoundingBox();
					if (Math.abs(cube.boundingBox.max.x - cube.boundingBox.min.x) <= 0.5) {
						cube.boundingBox.min.x = -0.5 * level.gridSize;
						cube.boundingBox.max.x = 0.5 * level.gridSize;
					}
					if (Math.abs(cube.boundingBox.max.z - cube.boundingBox.min.z) <= 0.5) {
						cube.boundingBox.min.z = -0.5 * level.gridSize;
						cube.boundingBox.max.z = 0.5 * level.gridSize;
					}
					var wallbody = new Physijs.BoxMesh(cube, dummy_material, 0);
					wallbody.position.copy(mesh.position);
					wallbody.visible = false;
					scene.add(wallbody);
				} else {

				}
			}
		}

		// Ceiling, no collision needed
		var ceiling_plane = new THREE.Mesh(
			new PlaneGeometry(level.gridSize * level.width, level.gridSize * level.depth,
				1, 1, "ny", level.width, level.depth),
			cache.getMaterial(level.materials.ceiling)
		);
		ceiling_plane.position.set(level.gridSize * level.width * 0.5, level.roomHeight, level.gridSize * level.depth * 0.5);
		scene.add(ceiling_plane);

		// Floor with collision
		var floor_plane = new Physijs.PlaneMesh(
			new PlaneGeometry(level.gridSize * level.width, level.gridSize * level.depth,
				1, 1, "py", level.width, level.depth),
			Physijs.createMaterial(cache.getMaterial(level.materials.floor), 0.9, 0.0), // friction, restitution
			0 // mass
		);
		floor_plane.position.set(level.gridSize * level.width * 0.5, 0.0, level.gridSize * level.depth * 0.5);
		floor_plane.receiveShadow = true;
		scene.add(floor_plane);

		// Level mesh
		geometry.computeTangents();
		mesh = new THREE.Mesh(geometry, new THREE.MeshFaceMaterial());
		mesh.receiveShadow = true;
		scene.add(mesh);
	};

	this.addLights = function(level) {
		// Torch model load callback
		function torchHandler(pos, rot) {
			return function(geometry) {
				for (var m = 0; m < geometry.materials.length; ++m)
					fixAnisotropy(geometry.materials[m]);
				var mat = geometry.materials.length > 1 ? new THREE.MeshFaceMaterial() : geometry.materials[0];
				var obj = new THREE.Mesh(geometry, mat);
				obj.position.copy(pos);
				obj.rotation.y = rot;
				obj.castShadow = true;
				obj.receiveShadow = true;
				scene.add(obj);
			};
		}

		// Ambient
		scene.add(new THREE.AmbientLight(0xaaaaaa));

		// Point lights
		var vec = new THREE.Vector2();
		for (var i = 0; i < level.lights.length; ++i) {
			// Actual light
			var light = new THREE.PointLight(0xffffaa, 1, 2 * level.gridSize);
			light.position.copy(level.lights[i].position);

			// Snap to wall
			// Create wall candidates for checking which wall is closest to the light
			vec.set(level.lights[i].position.x|0, level.lights[i].position.z|0);
			var candidates = [
				{ x: vec.x + 0.5, y: vec.y, a: Math.PI },
				{ x: vec.x + 1.0, y: vec.y + 0.5, a: Math.PI/2 },
				{ x: vec.x + 0.5, y: vec.y + 1.0, a: 0 },
				{ x: vec.x, y: vec.y + 0.5, a: -Math.PI/2 }
			];
			vec.set(level.lights[i].position.x, level.lights[i].position.z);
			// Find the closest
			var snapped = { d: 1000 };
			for (var j = 0; j < candidates.length; ++j) {
				candidates[j].d = vec.distanceToSquared(candidates[j]);
				if (candidates[j].d < snapped.d) snapped = candidates[j];
			}
			// Position the light to the wall
			light.position.x = snapped.x;
			light.position.z = snapped.y;
			// Move out the wall
			vec.set((level.lights[i].position.x|0) + 0.5, (level.lights[i].position.z|0) + 0.5);
			vec.subSelf(snapped).multiplyScalar(2);
			light.position.x += vec.x * 0.08;
			light.position.z += vec.y * 0.08;

			light.position.x *= level.gridSize;
			light.position.z *= level.gridSize;
			scene.add(light);
			lightManager.addLight(light);

			// Shadow casting light
			if (level.lights[i].target) {
				var light2 = new THREE.SpotLight(0xffffaa, light.intensity, light.distance);
				light2.position = light.position;
				light2.target.position.copy(level.lights[i].target);
				light2.angle = Math.PI / 2;
				light2.castShadow = true;
				light2.onlyShadow = true;
				light2.shadowCameraNear = 0.1 * UNIT;
				light2.shadowCameraFar = light.distance * 1.5 * UNIT;
				light2.shadowCameraFov = 100;
				light2.shadowBias = -0.0002;
				light2.shadowDarkness = 0.3;
				light2.shadowMapWidth = 256;
				light2.shadowMapHeight = 256;
				light2.shadowCameraVisible = false;
				scene.add(light2);
				lightManager.addShadow(light2);
			}

			// Mesh
			cache.loadModel("assets/models/torch/torch.js",
				torchHandler(new THREE.Vector3().copy(light.position), snapped.a));

			// Flame
			if (CONFIG.particles)
				light.emitter = createSimpleFire(light.position);
		}

		// Player's torch
		player.light = new THREE.PointLight(0x88bbff, 1, level.gridSize * 3);
		scene.add(player.light);
		player.shadow = new THREE.SpotLight(player.light.color, player.light.intensity, player.light.distance);
		player.shadow.angle = Math.PI / 4;
		player.shadow.onlyShadow = true;
		player.shadow.castShadow = true;
		player.shadow.shadowCameraNear = 0.1 * UNIT;
		player.shadow.shadowCameraFar = 10 * UNIT;
		player.shadow.shadowCameraFov = 90;
		player.shadow.shadowBias = -0.0002;
		player.shadow.shadowDarkness = 0.3;
		player.shadow.shadowMapWidth = 1024;
		player.shadow.shadowMapHeight = 1024;
		player.shadow.shadowCameraVisible = false;
		scene.add(player.shadow);
	};

	this.addObjects = function(level) {
		function objectHandler(pos, def) {
			return function(geometry) {
				if (!def) def = {};
				var obj, mass = def.mass || 0;
				var scale = 1.0;
				if (def.randScale) {
					scale += randf(-def.randScale, def.randScale);
					mass *= scale;
				}
				for (var m = 0; m < geometry.materials.length; ++m)
					fixAnisotropy(geometry.materials[m]);
				var mat = geometry.materials.length > 1 ? new THREE.MeshFaceMaterial() : geometry.materials[0];
				if (def.collision) {
					var material = Physijs.createMaterial(mat, 0.7, 0.2); // friction, restition
					if (def.collision == "plane")
						obj = new Physijs.PlaneMesh(geometry, material, mass);
					else if (def.collision == "box")
						obj = new Physijs.BoxMesh(geometry, material, mass);
					else if (def.collision == "sphere")
						obj = new Physijs.SphereMesh(geometry, material, mass);
					else if (def.collision == "cylinder")
						obj = new Physijs.CylinderMesh(geometry, material, mass);
					else if (def.collision == "cone")
						obj = new Physijs.ConeMesh(geometry, material, mass);
					else if (def.collision == "convex")
						obj = new Physijs.ConvexMesh(geometry, material, mass);
					else throw "Unsupported collision mesh type " + def.collision;
					self.objects.push(obj);
				} else {
					obj = new THREE.Mesh(geometry, mat);
				}
				pos.x *= level.gridSize;
				pos.z *= level.gridSize;
				// Auto-height
				if (pos.y === null) {
					if (!geometry.boundingBox) geometry.computeBoundingBox();
					pos.y = 0.5 * (geometry.boundingBox.max.y - geometry.boundingBox.min.y) + 0.001;
				}
				obj.position.copy(pos);
				obj.scale.set(scale, scale, scale);
				if (!def.noShadows) {
					obj.castShadow = true;
					obj.receiveShadow = true;
				}
				scene.add(obj);
			};
		}

		for (var i = 0; i < level.objects.length; ++i) {
			var objname = level.objects[i].name;
			var obj = cache.loadModel("assets/models/" + objname + "/" + objname + ".js",
				objectHandler(new THREE.Vector3().copy(level.objects[i].position), assets.objects[objname]));
		}
	};

	if (queryParams.level) {
		if (queryParams.level == "rand") {
			var gen = new MapGen();
			this.level = gen.generate();
		} else if (queryParams.level.length > 24) {
			var json = window.atob(queryParams.level);
			this.level = JSON.parse(json);
		} else {
			// TODO: Load the level named in the parameter
		}
	} else this.level = testLevel;

	this.level.get = this.level.get || function(x, z) {
		if (x < 0 || x >= this.width) return WALL;
		else if (z < 0 || z >= this.depth) return WALL;
		return this.map[z * this.width + x];
	};

	this.level.set = this.level.set || function(x, z, obj) {
		if (x < 0 || x >= this.width) return;
		else if (z < 0 || z >= this.depth) return;
		this.map[z * this.width + x] = obj;
	};

	// TODO: Set player rotation
	player.geometry.computeBoundingBox();
	player.position.x = this.level.start[0] * this.level.gridSize;
	player.position.y = 0.5 * (player.geometry.boundingBox.max.y - player.geometry.boundingBox.min.y) + 0.001;
	player.position.z = this.level.start[1] * this.level.gridSize;

	this.generateMesh(this.level);
	this.addLights(this.level);
	this.addObjects(this.level);
}
<|endoftext|>


<commit_before>import React from 'react'
import { Link } from 'react-router'
const { string } = React.PropTypes

const ShowCard = React.createClass({
  propTypes: {
    poster: string,
    title: string,
    year: string,
    description: string,
    imdbID: string
  },
  render () {
    const { poster, title, year, description, imdbID } = this.props
    return (
      <Link to='/details/${imdbID}' >

          <img src={`/public/img/posters/${poster}`} />

{title}

({year})

{description}}

      </Link>
    )
  }
})

export default ShowCard
<commit_msg>show card prop types are required<commit_after>import React from 'react'
import { Link } from 'react-router'
const { string } = React.PropTypes

const ShowCard = React.createClass({
  propTypes: {
    poster: string.isRequired,
    title: string.isRequired,
    year: string.isRequired,
    description: string.isRequired,
    imdbID: string.isRequiredg
  },
  render () {
    const { poster, title, year, description, imdbID } = this.props
    return (
      <Link to='/details/${imdbID}' >

          <img src={`/public/img/posters/${poster}`} />

{title}

({year})

{description}}

      </Link>
    )
  }
})

export default ShowCard
<|endoftext|>


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

  var intakeFilters = angular.module('intakeFilters', []);

  intakeFilters.filter('schemaTypeFilter', function () {
    return function (input, filterKey) {
      var filteredInput = {};

      if (filterKey === undefined) {
        return input;
      }

      var searchKeys = filterKey;
      if (typeof(searchKeys) === 'object') {
        searchKeys = Object.keys(searchKeys);
      }

      angular.forEach(input, function (value, key) {
        if (typeof(searchKeys) === 'string') {
          if (value.label && value.label.toLowerCase().contains(filterKey.toLowerCase())) {
            filteredInput[key] = value;
          }
        }
      });
      return filteredInput;
    };
  });

})(window.angular);
<commit_msg>Moved filter stuff to controller for schema<commit_after>(function (angular) {
  'use strict';

  var intakeFilters = angular.module('intakeFilters', []);

  intakeFilters.filter('schemaTypeFilter', function () {
    return function (input, filterKey) {
      var filteredInput = {};

      angular.forEach(input, function (value, key) {
        if (value.ancestors.indexOf(filterKey.subtype) === 0) {
          if (value.ancestors.length === 1) {
            filteredInput[key] = value;
          }
        }
        else if (value.ancestors.indexOf(filterKey.subtype) !== -1) {
          filteredInput[key] = value;
        }

        if (filterKey.label !== undefined && filterKey.label !== '') {
          if (value.label && !value.label.toLowerCase().contains(filterKey.label.toLowerCase())) {
            delete filteredInput[key];
          }
        }
      });

      return filteredInput;
    };
  });

})(window.angular);
<|endoftext|>


<commit_before>i18n = {}

i18n.db = new Mongo.Collection('halunka:i18n')
i18n.state = new ReactiveDict('i18nValues')
i18n.state.set('langs', {})
i18n.dep = new Tracker.Dependency
i18n.depChanged = i18n.dep.changed.bind(i18n.dep)

i18n.add = function i18nAdd (dataArr, cb) {
  var done = cb ?
    _.compose(cb, i18n.depChanged):
    i18n.depChanged
  dataArr = [].concat(dataArr)
  for(var i = 0; i < dataArr.length - 1; i++) {
    i18n.insert(dataArr[i])
  }
  i18n.insert(dataArr[i], done)
}

i18n.insert = function i18nInsert (translations, done) {
  var oldId
  var lang = i18n.getDefaultLanguage()
  oldId = ( i18n.db.findOne(constructObject(lang, translations[lang])) || {} )._id
  translations = _.extend(i18n.state.get('langs'), translations)
  if(oldId) return i18n.db.update(oldId, { $set: translations }, done)
  return i18n.db.insert(translations, done)
}

i18n.get = function i18nget (key, lang) {
  lang = typeof lang == 'string' ? lang : i18n.getLanguage()
  return maybeGet(i18n.reactiveQuery(key), lang)
}

i18n.getAll = function i18nGetAll (key) {
  lang = typeof lang == 'string' ? lang : i18n.getLanguage()
  return _.omit(i18n.reactiveQuery(key), '_id')
}

i18n.reactiveQuery = function i18nReactiveQuery (key) {
  i18n.dep.depend()
  return i18n.db.findOne(constructObject(i18n.getDefaultLanguage(), key))
}

i18n.setDefaultLanguage = function i18nSetDefaultLang (newValue) {
  i18n.state.set('default', newValue)
  return newValue
}

i18n.getDefaultLanguage = function i18nGetDefaultLanguage () {
  return i18n.state.get('default') ||
    _.keys(i18n.state.get('langs') || [])[0]
}

i18n.addLanguage = function i18nAddLanguage (key, str) {
  var langs = i18n.state.get('langs') || {}
  langs[key] = str
  i18n.state.set('langs', langs)
}

i18n.setLanguage = function i18nSet (lang) {
  return i18n.state.set('currLang', lang)
}

i18n.getLanguage = function i18nGet () {
  return i18n.state.get('currLang') ||
    i18n.getDefaultLanguage()
}

i18n.listLanguages = function i18nListLanguages () {
  return _.map(i18n.state.get('langs'), function (val, key) {
    return {
      key: key,
      name: val
    }
  })
}

function maybeGet (obj, key) {
  return obj ? obj[key] : ''
}

function constructObject (key, value) {
  var obj = {}
  obj[key] = value
  return obj
}

if(Meteor.isServer) {
  Meteor.publish('i18n:all', function () {
    return i18n.db.find()
  })
  Meteor.publish('i18n:specific', function (lang) {
    return i18n.db.find({ fields: [lang] })
  })
}

if(Meteor.isClient) {
  Template.registerHelper('i18nget', i18n.get)
  Template.registerHelper('i18nlist', i18n.listLanguage)

  i18n.loadSpecific = function i18nLoadSpecific (lang, cb) {
    Meteor.subscribe('i18n:specific', lang, function () {
      i18n.depChanged()
      if(cb) cb()
    })
  }

  i18n.loadAll = function i18nLoadAll (cb) {
    Meteor.subscribe('i18n:all', function () {
      i18n.depChanged()
      if(cb) cb()
    })
  }
}
<commit_msg>Implement i18n.parseJSONFile<commit_after>i18n = {}

i18n.db = new Mongo.Collection('halunka:i18n')
i18n.state = new ReactiveDict('i18nValues')
i18n.state.set('langs', {})
i18n.dep = new Tracker.Dependency
i18n.depChanged = i18n.dep.changed.bind(i18n.dep)

i18n.add = function i18nAdd (dataArr, cb) {
  var done = cb ?
    _.compose(cb, i18n.depChanged):
    i18n.depChanged
  dataArr = [].concat(dataArr)
  for(var i = 0; i < dataArr.length - 1; i++) {
    i18n.insert(dataArr[i])
  }
  i18n.insert(dataArr[i], done)
  return dataArr
}

i18n.insert = function i18nInsert (translations, done) {
  var oldId
  var lang = i18n.getDefaultLanguage()
  oldId = ( i18n.db.findOne(constructObject(lang, translations[lang])) || {} )._id
  translations = _.extend(i18n.state.get('langs'), translations)
  if(oldId) return i18n.db.update(oldId, { $set: translations }, done)
  return i18n.db.insert(translations, done)
}

i18n.get = function i18nget (key, lang) {
  lang = typeof lang == 'string' ? lang : i18n.getLanguage()
  return maybeGet(i18n.reactiveQuery(key), lang)
}

i18n.getAll = function i18nGetAll (key) {
  lang = typeof lang == 'string' ? lang : i18n.getLanguage()
  return _.omit(i18n.reactiveQuery(key), '_id')
}

i18n.reactiveQuery = function i18nReactiveQuery (key) {
  i18n.dep.depend()
  return i18n.db.findOne(constructObject(i18n.getDefaultLanguage(), key))
}

i18n.setDefaultLanguage = function i18nSetDefaultLang (newValue) {
  i18n.state.set('default', newValue)
  return newValue
}

i18n.getDefaultLanguage = function i18nGetDefaultLanguage () {
  return i18n.state.get('default') ||
    _.keys(i18n.state.get('langs') || [])[0]
}

i18n.addLanguage = function i18nAddLanguage (key, str) {
  var langs = i18n.state.get('langs') || {}
  langs[key] = str
  i18n.state.set('langs', langs)
}

i18n.setLanguage = function i18nSet (lang) {
  return i18n.state.set('currLang', lang)
}

i18n.getLanguage = function i18nGet () {
  return i18n.state.get('currLang') ||
    i18n.getDefaultLanguage()
}

i18n.listLanguages = function i18nListLanguages () {
  return _.map(i18n.state.get('langs'), function (val, key) {
    return {
      key: key,
      name: val
    }
  })
}

function maybeGet (obj, key) {
  return obj ? obj[key] : ''
}

function constructObject (key, value) {
  var obj = {}
  obj[key] = value
  return obj
}

if(Meteor.isServer) {
  var fs = require('fs')

  Meteor.publish('i18n:all', function () {
    return i18n.db.find()
  })
  Meteor.publish('i18n:specific', function (lang) {
    return i18n.db.find({ fields: [lang] })
  })

  i18n.parseJSONFile = function i18nParseJSONFile (path) {
    if(!fs.existsSync(path)) throw 'halunka:i18n: File doesn\'t exist: ' + path
    return i18n.add(JSON.parse(fs.readFileSync(path)))
  }

} else {
  Template.registerHelper('i18nget', i18n.get)
  Template.registerHelper('i18nlist', i18n.listLanguage)

  i18n.loadSpecific = function i18nLoadSpecific (lang, cb) {
    Meteor.subscribe('i18n:specific', lang, function () {
      i18n.depChanged()
      if(cb) cb()
    })
  }

  i18n.loadAll = function i18nLoadAll (cb) {
    Meteor.subscribe('i18n:all', function () {
      i18n.depChanged()
      if(cb) cb()
    })
  }
}
<|endoftext|>


<commit_before>class Tutorial {
	constructor() {
		var self = this;

		// check localstorage to see whether we should start the tut or not
		if (localStorage.getItem("tour_end") == null && window.tourInProgress != true) {
			// Set a global defining our progress
			window.tourInProgress = true;

			setTimeout(function () {
				self.openAccordion();
			}, 500);
		}
	}

	/*
		Open the first top level for every level
	*/
	openAccordion() {
		this.openedAccordion = true;

		this.openChildRow($('#classdatawraper').children(0));
	}

	/*
		Opens the first row in the child of the specified element
	*/
	openChildRow(element) {
		var self = this;

		// Get the row
		var row = element.parent().find('.has-children').eq(0);

		if (row.length > 0) {

			// Ensure the row isn't open already, if not, click it
			if (row.find("ul").length == 1 && row.find(".accordiontableparent").length == 0) row.find('label').click();

			// Call the next row
			setTimeout(function () {
				self.openChildRow(row.find('label').eq(0));
			}, 50);
		}
		else {
			// start up the tour
			self.createIntro();
		}
	}

	/*
		Initialize and start the tour
	*/
	createIntro() {
		var self = this;

		var tour = new Tour({
		  	steps: [
			  	{
			    	title: "What is this?",
			    	content: "Schedule Storm is a student schedule generator that lets you input your courses and preferences to generate possible schedules.

You can always restart this tour by going to preferences"
			  	},
			  	{
			  		element: document.querySelector('#classdatawraper'),
			    	title: "Course List",
			    	content: "In this accordion, you can add and look at courses. Clicking on labels opens their contents."
			  	},
			  	{
			  		element: $("#classdatawraper").find('.addCourseButton')[0],
			  		title: "Add Courses",
			  		content: "If you want to add a course, simply click on the 'plus' icon next to its name"
			  	},
			  	{
			  		element: $("#classdatawraper").find('[classid]')[0],
			  		title: "Add Specific Classes",
			  		content: "If you want to add a specific class, you can click on the 'plus' icon next to it.

All other required classes will automatically be filled by the generator"
			  	},
			  	{
			  		element: $("#classdatawraper").find("td")[1],
			  		title: "Rate My Professor Ratings",
			  		content: "If you see a number beside a teacher's name, that is their Rate My Professor rating out of 5

You can specify the weighting of the RMP rating in the generator in preferences"
			  	},
			  	{
			  		element: $("#searchcourses")[0],
			  		title: "Search Courses",
			  		content: "Here you can search for teachers, courses, classes, rooms, descriptions, faculties, subjects, prerequisites...

Almost anything!"
			  	},
			  	{
			  		element: $("#locationselect")[0],
			  		title: "Change Location",
			  		content: "You can limit the location for classes to specific campuses or areas"
			  	},
			  	{
			  		element: $("#courseSelector").find(".input-group-btn")[1],
			  		title: "Change Term",
			  		content: "You can change the term you're viewing in this university"
			  	},
			  	{
			  		element: $("#MyCourses"),
			  		title: "My Courses",
			  		content: "All of your chosen courses are displayed here",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#coursegroups"),
			  		title: "Course Groups",
			  		content: "You can create groups of courses where the generator fulfills every group. You can change/remove the group type by clicking on its 'pill'"
			  	},
			  	{
			  		element: $("#addGroupbtn"),
			  		title: "Adding Course Groups",
			  		content: "Clicking this will create a new course group

This is useful for electives where you only want one or two of the courses selected in the group"
			  	},
			  	{
			  		element: $("#schedule"),
			  		title: "Calendar",
			  		content: "You can look through possible schedules on this calendar",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#calendarStatus"),
			  		title: "Blocking Timeslots",
			  		content: "You can block specific timeslots for the generator by clicking and dragging on the calendar

Clicking on a banned timeslot will unban it",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#prevSchedule"),
			  		title: "Browsing Schedules",
			  		content: "You can browse possible schedules by clicking the previous and next buttons here",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#scheduleutilities"),
			  		title: "Schedule Utilities",
			  		content: "Useful schedule utilities can be found here, you can:
* Download a picture of your schedule
* Copy your schedule to clipboard
* Remove all blocked timeslots"
			  	},
			  	{
			  		element: $("#preferencesbutton"),
			  		title: "Preferences",
			  		content: "You can change your schedule preferences and edit settings by clicking this button

You can change your preferences for morning/night classes, consecutive classes, and teacher quality over time slots.

You can also specify that you only want the generator to allow open classes (some Unis have custom settings)",
			  		placement: "left",
			  	},
			  	{
			  		element: $("#MyUniversity"),
			  		title: "Change University",
			  		content: "You can click here to open a dropdown and change your university",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#aboutbutton"),
			  		title: "About Us",
			  		content: "We are two Computer Science students that thought making university schedules should be easier

Please contact us using Github or Email if you'd like to say 'Hi', file a bug report, want us to add your university, or add a new feature!",
			  		placement: "left"
			  	},
			  	{
			  		title: "That ended too soon!",
			  		content: "It looks like thats the end of our tour, remember you can always look at it again by going to preferences.

This project is completely open-source on Github and if you want to implement your university or add a feature, please do it!"
			  	}
			],
			backdrop: true,
			orphan: true,
			onEnd: function (tour) {
				window.tourInProgress = false;
			}
		});

		// Initialize the tour
		tour.init();

		// Start the tour
		tour.start().goTo(0);
	}
}
<commit_msg>Tut: Fixes grammar issues<commit_after>class Tutorial {
	constructor() {
		var self = this;

		// check localstorage to see whether we should start the tut or not
		if (localStorage.getItem("tour_end") == null && window.tourInProgress != true) {
			// Set a global defining our progress
			window.tourInProgress = true;

			setTimeout(function () {
				self.openAccordion();
			}, 500);
		}
	}

	/*
		Open the first top level for every level
	*/
	openAccordion() {
		this.openedAccordion = true;

		this.openChildRow($('#classdatawraper').children(0));
	}

	/*
		Opens the first row in the child of the specified element
	*/
	openChildRow(element) {
		var self = this;

		// Get the row
		var row = element.parent().find('.has-children').eq(0);

		if (row.length > 0) {

			// Ensure the row isn't open already, if not, click it
			if (row.find("ul").length == 1 && row.find(".accordiontableparent").length == 0) row.find('label').click();

			// Call the next row
			setTimeout(function () {
				self.openChildRow(row.find('label').eq(0));
			}, 50);
		}
		else {
			// start up the tour
			self.createIntro();
		}
	}

	/*
		Initialize and start the tour
	*/
	createIntro() {
		var self = this;

		var tour = new Tour({
		  	steps: [
			  	{
			    	title: "What is this?",
			    	content: "Schedule Storm is a student schedule generator that lets you input your courses and preferences to generate possible schedules.

You can always restart this tour by going to preferences"
			  	},
			  	{
			  		element: document.querySelector('#classdatawraper'),
			    	title: "Course List",
			    	content: "In this accordion, you can add and look at courses. Clicking on labels opens their contents."
			  	},
			  	{
			  		element: $("#classdatawraper").find('.addCourseButton')[0],
			  		title: "Add Courses",
			  		content: "If you want to add a course, simply click on the 'plus' icon next to its name"
			  	},
			  	{
			  		element: $("#classdatawraper").find('[classid]')[0],
			  		title: "Add Specific Classes",
			  		content: "If you want to add a specific class, you can click on the 'plus' icon next to it.

All other required classes will automatically be filled by the generator"
			  	},
			  	{
			  		element: $("#classdatawraper").find("td")[1],
			  		title: "Rate My Professor Ratings",
			  		content: "If you see a number beside a teacher's name, that is their Rate My Professor rating out of 5

You can specify the weighting of the RMP rating in the generator in preferences"
			  	},
			  	{
			  		element: $("#searchcourses")[0],
			  		title: "Search Courses",
			  		content: "Here you can search for teachers, courses, classes, rooms, descriptions, faculties, subjects, prerequisites...

Almost anything!"
			  	},
			  	{
			  		element: $("#locationselect")[0],
			  		title: "Change Location",
			  		content: "You can limit the location for classes to specific campuses or areas"
			  	},
			  	{
			  		element: $("#courseSelector").find(".input-group-btn")[1],
			  		title: "Change Term",
			  		content: "You can change the term you're viewing in this university"
			  	},
			  	{
			  		element: $("#MyCourses"),
			  		title: "My Courses",
			  		content: "All of your chosen courses are displayed here",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#coursegroups"),
			  		title: "Course Groups",
			  		content: "You can create groups of courses where the generator fulfills every group. You can change/remove the group type by clicking on its 'pill'"
			  	},
			  	{
			  		element: $("#addGroupbtn"),
			  		title: "Adding Course Groups",
			  		content: "Clicking this will create a new course group

This is useful for electives where you only want one or two of the courses selected in the group"
			  	},
			  	{
			  		element: $("#schedule"),
			  		title: "Calendar",
			  		content: "You can look through possible schedules on this calendar",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#calendarStatus"),
			  		title: "Blocking Timeslots",
			  		content: "You can block specific timeslots for the generator by clicking and dragging on the calendar

Clicking on a banned timeslot will unban it",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#prevSchedule"),
			  		title: "Browsing Schedules",
			  		content: "You can browse possible schedules by clicking the previous and next buttons here",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#scheduleutilities"),
			  		title: "Schedule Utilities",
			  		content: "Useful schedule utilities can be found here, you can:
* Download a picture of your schedule
* Copy your schedule to clipboard
* Remove all blocked timeslots"
			  	},
			  	{
			  		element: $("#preferencesbutton"),
			  		title: "Preferences",
			  		content: "You can change your schedule preferences and edit settings by clicking this button

You can change your preferences for morning/night classes, consecutive classes, and teacher quality over time slots.

You can also specify that you only want the generator to allow open classes (some universities have custom settings)",
			  		placement: "left",
			  	},
			  	{
			  		element: $("#MyUniversity"),
			  		title: "Change University",
			  		content: "You can click here to open a dropdown and change your university",
			  		placement: "left"
			  	},
			  	{
			  		element: $("#aboutbutton"),
			  		title: "About Us",
			  		content: "We are two Computer Science students that thought making university schedules should be easier

Please contact us using Github or Email if you'd like to say 'Hi', file a bug report, want us to add your university, or add a new feature!",
			  		placement: "left"
			  	},
			  	{
			  		title: "That ended too soon!",
			  		content: "It looks like thats the end of our tour, remember you can always look at it again by going to preferences.

This project is completely open-source on Github and if you want to implement your university or add a feature, please do it!"
			  	}
			],
			backdrop: true,
			orphan: true,
			onEnd: function (tour) {
				window.tourInProgress = false;
			}
		});

		// Initialize the tour
		tour.init();

		// Start the tour
		tour.start().goTo(0);
	}
}
<|endoftext|>


<commit_before>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');
const Precise = require ('./base/Precise');

//  ---------------------------------------------------------------------------

module.exports = class flowbtc extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'flowbtc',
            'name': 'flowBTC',
            'countries': [ 'BR' ], // Brazil
            'version': 'v1',
            'rateLimit': 1000,
            'has': {
                'cancelOrder': true,
                'CORS': false,
                'createOrder': true,
                'fetchBalance': true,
                'fetchMarkets': true,
                'fetchOrderBook': true,
                'fetchTicker': true,
                'fetchTrades': true,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/51840849/87443317-01c0d080-c5fe-11ea-95c2-9ebe1a8fafd9.jpg',
                'api': 'https://publicapi.flowbtc.com.br',
                'www': 'https://www.flowbtc.com.br',
                'doc': 'https://www.flowbtc.com.br/api.html',
            },
            'requiredCredentials': {
                'apiKey': true,
                'secret': true,
                'uid': true,
            },
            'api': {
                'public': {
                    'post': [
                        'GetTicker',
                        'GetTrades',
                        'GetTradesByDate',
                        'GetOrderBook',
                        'GetProductPairs',
                        'GetProducts',
                    ],
                },
                'private': {
                    'post': [
                        'CreateAccount',
                        'GetUserInfo',
                        'SetUserInfo',
                        'GetAccountInfo',
                        'GetAccountTrades',
                        'GetDepositAddresses',
                        'Withdraw',
                        'CreateOrder',
                        'ModifyOrder',
                        'CancelOrder',
                        'CancelAllOrders',
                        'GetAccountOpenOrders',
                        'GetOrderFee',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'tierBased': false,
                    'percentage': true,
                    'maker': 0.0025,
                    'taker': 0.005,
                },
            },
        });
    }

    async fetchMarkets (params = {}) {
        const response = await this.publicPostGetProductPairs (params);
        const markets = this.safeValue (response, 'productPairs');
        const result = {};
        for (let i = 0; i < markets.length; i++) {
            const market = markets[i];
            const id = this.safeString (market, 'name');
            const baseId = this.safeString (market, 'product1Label');
            const quoteId = this.safeString (market, 'product2Label');
            const base = this.safeCurrencyCode (baseId);
            const quote = this.safeCurrencyCode (quoteId);
            const precision = {
                'amount': this.safeInteger (market, 'product1DecimalPlaces'),
                'price': this.safeInteger (market, 'product2DecimalPlaces'),
            };
            const symbol = base + '/' + quote;
            result[symbol] = {
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'price': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'cost': {
                        'min': undefined,
                        'max': undefined,
                    },
                },
                'info': market,
                'active': undefined,
            };
        }
        return result;
    }

    async fetchBalance (params = {}) {
        await this.loadMarkets ();
        const response = await this.privatePostGetAccountInfo (params);
        const balances = this.safeValue (response, 'currencies');
        const result = { 'info': response };
        for (let i = 0; i < balances.length; i++) {
            const balance = balances[i];
            const currencyId = balance['name'];
            const code = this.safeCurrencyCode (currencyId);
            const account = this.account ();
            account['free'] = this.safeString (balance, 'balance');
            account['total'] = this.safeString (balance, 'hold');
            result[code] = account;
        }
        return this.parseBalance (result);
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'productPair': market['id'],
        };
        const response = await this.publicPostGetOrderBook (this.extend (request, params));
        return this.parseOrderBook (response, symbol, undefined, 'bids', 'asks', 'px', 'qty');
    }

    async fetchTicker (symbol, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'productPair': market['id'],
        };
        const ticker = await this.publicPostGetTicker (this.extend (request, params));
        const timestamp = this.milliseconds ();
        const last = this.safeNumber (ticker, 'last');
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': this.safeNumber (ticker, 'high'),
            'low': this.safeNumber (ticker, 'low'),
            'bid': this.safeNumber (ticker, 'bid'),
            'bidVolume': undefined,
            'ask': this.safeNumber (ticker, 'ask'),
            'askVolume': undefined,
            'vwap': undefined,
            'open': undefined,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': undefined,
            'percentage': undefined,
            'average': undefined,
            'baseVolume': this.safeNumber (ticker, 'volume24hr'),
            'quoteVolume': this.safeNumber (ticker, 'volume24hrProduct2'),
            'info': ticker,
        };
    }

    parseTrade (trade, market) {
        const timestamp = this.safeTimestamp (trade, 'unixtime');
        const side = (trade['incomingOrderSide'] === 0) ? 'buy' : 'sell';
        const id = this.safeString (trade, 'tid');
        const priceString = this.safeString (trade, 'px');
        const amountString = this.safeString (trade, 'qty');
        const price = this.parseNumber (priceString);
        const amount = this.parseNumber (amountString);
        const cost = this.parseNumber (Precise.stringMul (priceString, amountString));
        return {
            'info': trade,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': market['symbol'],
            'id': id,
            'order': undefined,
            'type': undefined,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': cost,
            'takerOrMaker': undefined,
            'fee': undefined,
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'ins': market['id'],
            'startIndex': -1,
        };
        const response = await this.publicPostGetTrades (this.extend (request, params));
        return this.parseTrades (response['trades'], market, since, limit);
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        await this.loadMarkets ();
        const orderType = (type === 'market') ? 1 : 0;
        const request = {
            'ins': this.marketId (symbol),
            'side': side,
            'orderType': orderType,
            'qty': amount,
            'px': this.priceToPrecision (symbol, price),
        };
        const response = await this.privatePostCreateOrder (this.extend (request, params));
        return {
            'info': response,
            'id': response['serverOrderId'],
        };
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        if ('ins' in params) {
            const request = {
                'serverOrderId': id,
            };
            return await this.privatePostCancelOrder (this.extend (request, params));
        }
        throw new ExchangeError (this.id + ' cancelOrder() requires an `ins` symbol parameter for cancelling an order');
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        const url = this.urls['api'] + '/' + this.version + '/' + path;
        if (api === 'public') {
            if (Object.keys (params).length) {
                body = this.json (params);
            }
        } else {
            this.checkRequiredCredentials ();
            const nonce = this.nonce ();
            const auth = nonce.toString () + this.uid + this.apiKey;
            const signature = this.hmac (this.encode (auth), this.encode (this.secret));
            body = this.json (this.extend ({
                'apiKey': this.apiKey,
                'apiNonce': nonce,
                'apiSig': signature.toUpperCase (),
            }, params));
            headers = {
                'Content-Type': 'application/json',
            };
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined, config = {}, context = {}) {
        const response = await this.fetch2 (path, api, method, params, headers, body, config, context);
        if ('isAccepted' in response) {
            if (response['isAccepted']) {
                return response;
            }
        }
        throw new ExchangeError (this.id + ' ' + this.json (response));
    }
};
<commit_msg>flowbtc handleErrors update<commit_after>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');
const Precise = require ('./base/Precise');

//  ---------------------------------------------------------------------------

module.exports = class flowbtc extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'flowbtc',
            'name': 'flowBTC',
            'countries': [ 'BR' ], // Brazil
            'version': 'v1',
            'rateLimit': 1000,
            'has': {
                'cancelOrder': true,
                'CORS': false,
                'createOrder': true,
                'fetchBalance': true,
                'fetchMarkets': true,
                'fetchOrderBook': true,
                'fetchTicker': true,
                'fetchTrades': true,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/51840849/87443317-01c0d080-c5fe-11ea-95c2-9ebe1a8fafd9.jpg',
                'api': 'https://publicapi.flowbtc.com.br',
                'www': 'https://www.flowbtc.com.br',
                'doc': 'https://www.flowbtc.com.br/api.html',
            },
            'requiredCredentials': {
                'apiKey': true,
                'secret': true,
                'uid': true,
            },
            'api': {
                'public': {
                    'post': [
                        'GetTicker',
                        'GetTrades',
                        'GetTradesByDate',
                        'GetOrderBook',
                        'GetProductPairs',
                        'GetProducts',
                    ],
                },
                'private': {
                    'post': [
                        'CreateAccount',
                        'GetUserInfo',
                        'SetUserInfo',
                        'GetAccountInfo',
                        'GetAccountTrades',
                        'GetDepositAddresses',
                        'Withdraw',
                        'CreateOrder',
                        'ModifyOrder',
                        'CancelOrder',
                        'CancelAllOrders',
                        'GetAccountOpenOrders',
                        'GetOrderFee',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'tierBased': false,
                    'percentage': true,
                    'maker': 0.0025,
                    'taker': 0.005,
                },
            },
        });
    }

    async fetchMarkets (params = {}) {
        const response = await this.publicPostGetProductPairs (params);
        const markets = this.safeValue (response, 'productPairs');
        const result = {};
        for (let i = 0; i < markets.length; i++) {
            const market = markets[i];
            const id = this.safeString (market, 'name');
            const baseId = this.safeString (market, 'product1Label');
            const quoteId = this.safeString (market, 'product2Label');
            const base = this.safeCurrencyCode (baseId);
            const quote = this.safeCurrencyCode (quoteId);
            const precision = {
                'amount': this.safeInteger (market, 'product1DecimalPlaces'),
                'price': this.safeInteger (market, 'product2DecimalPlaces'),
            };
            const symbol = base + '/' + quote;
            result[symbol] = {
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'price': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'cost': {
                        'min': undefined,
                        'max': undefined,
                    },
                },
                'info': market,
                'active': undefined,
            };
        }
        return result;
    }

    async fetchBalance (params = {}) {
        await this.loadMarkets ();
        const response = await this.privatePostGetAccountInfo (params);
        const balances = this.safeValue (response, 'currencies');
        const result = { 'info': response };
        for (let i = 0; i < balances.length; i++) {
            const balance = balances[i];
            const currencyId = balance['name'];
            const code = this.safeCurrencyCode (currencyId);
            const account = this.account ();
            account['free'] = this.safeString (balance, 'balance');
            account['total'] = this.safeString (balance, 'hold');
            result[code] = account;
        }
        return this.parseBalance (result);
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'productPair': market['id'],
        };
        const response = await this.publicPostGetOrderBook (this.extend (request, params));
        return this.parseOrderBook (response, symbol, undefined, 'bids', 'asks', 'px', 'qty');
    }

    async fetchTicker (symbol, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'productPair': market['id'],
        };
        const ticker = await this.publicPostGetTicker (this.extend (request, params));
        const timestamp = this.milliseconds ();
        const last = this.safeNumber (ticker, 'last');
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': this.safeNumber (ticker, 'high'),
            'low': this.safeNumber (ticker, 'low'),
            'bid': this.safeNumber (ticker, 'bid'),
            'bidVolume': undefined,
            'ask': this.safeNumber (ticker, 'ask'),
            'askVolume': undefined,
            'vwap': undefined,
            'open': undefined,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': undefined,
            'percentage': undefined,
            'average': undefined,
            'baseVolume': this.safeNumber (ticker, 'volume24hr'),
            'quoteVolume': this.safeNumber (ticker, 'volume24hrProduct2'),
            'info': ticker,
        };
    }

    parseTrade (trade, market) {
        const timestamp = this.safeTimestamp (trade, 'unixtime');
        const side = (trade['incomingOrderSide'] === 0) ? 'buy' : 'sell';
        const id = this.safeString (trade, 'tid');
        const priceString = this.safeString (trade, 'px');
        const amountString = this.safeString (trade, 'qty');
        const price = this.parseNumber (priceString);
        const amount = this.parseNumber (amountString);
        const cost = this.parseNumber (Precise.stringMul (priceString, amountString));
        return {
            'info': trade,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': market['symbol'],
            'id': id,
            'order': undefined,
            'type': undefined,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': cost,
            'takerOrMaker': undefined,
            'fee': undefined,
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'ins': market['id'],
            'startIndex': -1,
        };
        const response = await this.publicPostGetTrades (this.extend (request, params));
        return this.parseTrades (response['trades'], market, since, limit);
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        await this.loadMarkets ();
        const orderType = (type === 'market') ? 1 : 0;
        const request = {
            'ins': this.marketId (symbol),
            'side': side,
            'orderType': orderType,
            'qty': amount,
            'px': this.priceToPrecision (symbol, price),
        };
        const response = await this.privatePostCreateOrder (this.extend (request, params));
        return {
            'info': response,
            'id': response['serverOrderId'],
        };
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        if ('ins' in params) {
            const request = {
                'serverOrderId': id,
            };
            return await this.privatePostCancelOrder (this.extend (request, params));
        }
        throw new ExchangeError (this.id + ' cancelOrder() requires an `ins` symbol parameter for cancelling an order');
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        const url = this.urls['api'] + '/' + this.version + '/' + path;
        if (api === 'public') {
            if (Object.keys (params).length) {
                body = this.json (params);
            }
        } else {
            this.checkRequiredCredentials ();
            const nonce = this.nonce ();
            const auth = nonce.toString () + this.uid + this.apiKey;
            const signature = this.hmac (this.encode (auth), this.encode (this.secret));
            body = this.json (this.extend ({
                'apiKey': this.apiKey,
                'apiNonce': nonce,
                'apiSig': signature.toUpperCase (),
            }, params));
            headers = {
                'Content-Type': 'application/json',
            };
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    handleErrors (httpCode, reason, url, method, headers, body, response, requestHeaders, requestBody) {
        if (response === undefined) {
            return;
        }
        const isAccepted = this.safeValue (response, 'isAccepted');
        if (!isAccepted) {
            throw new ExchangeError (this.id + ' ' + this.json (response));
        }
    }
};
<|endoftext|>