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

/**
 * Creates a Property that does synchronization of values with a swappable Property that itself can change.
 * Handles the case where you need a Property that can switch between acting like multiple other Properties.
 *
 * With no other options specified, the value of this Property is:
 * - null, if valuePropertyProperty.value === null
 * - valuePropertyProperty.value.value otherwise
 *
 * The value of this Property (generalized, with the options available) is:
 * - derive( defaultValue ), if valuePropertyProperty.value === null
 * - map( derive( valuePropertyProperty.value ).value ) otherwise
 *
 * Generally, this DynamicProperty uses one-way synchronization (it only listens to the source), but if the
 * 'bidirectional' option is true, it will use two-way synchronization (changes to this Property will change the active
 * source). Thus when this Property changes value (when bidirectional is true), it will set:
 * - derive( valuePropertyProperty.value ).value = inverseMap( this.value ), if valuePropertyProperty.value !== null
 *
 *******************************
 * General example
 *******************************
 *   var firstProperty = new Property( Color.RED );
 *   var secondProperty = new Property( Color.BLUE );
 *   var currentProperty = new Property( firstProperty ); // {Property.<Property.<Color>>}
 *
 *   var backgroundFill = new DynamicProperty( currentProperty ) // Turns into a {Property.<Color>}
 *   backgroundFill.value; // Color.RED, since: currentProperty.value === firstProperty and
 *                                              firstProperty.value === Color.RED
 *   firstProperty.value = Color.YELLOW;
 *   backgroundFill.value; // Color.YELLOW - It's connected to firstProperty right now
 *
 *   currentProperty.value = secondProperty;
 *   backgroundFill.value; // Color.BLUE - It's the secondProperty's value
 *
 *   secondProperty.value = Color.MAGENTA;
 *   backgroundFill.value; // Color.MAGENTA - Yes, it's listening to the other Property now.
 *
 * Also supports falling back to null if our main Property is set to null:
 *   currentProperty.value = null;
 *   backgroundFill.value; // null
 *
 *******************************
 * 'derive' option
 *******************************
 * Additionally, DynamicProperty supports the ability to derive the Property value from our main Property's value.
 * For example, say you have multiple scenes each with the type:
 *   scene: {
 *     backgroundColorProperty: {Property.<Color>}
 *   }
 * and you have a currentSceneProperty: {Property.<Scene>}, you may want to create:
 *   var currentBackgroundColorProperty = new DynamicProperty( currentSceneProperty, {
 *     derive: 'backgroundColorProperty'
 *   } );
 * This would always report the current scene's current background color.
 * What if you sometimes don't have a scene active, e.g. {Property.<Scene|null>}? You can provide a default value:
 *  new DynamicProperty( currentSceneProperty, {
 *    derive: 'backgroundColorProperty',
 *    defaultValue: Color.BLACK
 *  } );
 * So that if the currentSceneProperty's value is null, the value of our DynamicProperty will be Color.BLACK.
 *
 *******************************
 * 'bidirectional' option
 *******************************
 * If you would like for direct changes to this Property to change the original source (bidirectional synchronization),
 * then pass bidirectional:true:
 *   var firstProperty = new Property( 5 );
 *   var secondProperty = new Property( 10 );
 *   var numberPropertyProperty = new Property( firstProperty );
 *   var dynamicProperty = new DynamicProperty( numberPropertyProperty, { bidirectional: true } );
 *   dynamicProperty.value = 2; // allowed now that it is bidrectional, otherwise prohibited
 *   firstProperty.value; // 2
 *   numberPropertyProperty.value = secondProperty; // change which Property is active
 *   dynamicProperty.value; // 10, from the new Property
 *   dynamicProperty.value = 0;
 *   secondProperty.value; // 0, set above.
 *   firstProperty.value; // still 2 from above, since our dynamic Property switched to the other Property
 *
 *******************************
 * 'map' and 'inverseMap' options
 *******************************
 * DynamicProperty also supports mapping values to different types. For example, say we have a
 * numberPropertyProperty {Property.<Property.<number>>}, but want to have a {Property.<string>} as the output. Then:
 *   new DynamicProperty( numberPropertyProperty, {
 *     map: function( number ) { return '' + number; }
 *   } );
 * will do the trick. If this needs to be done with a bidirectional DynamicProperty, also include inverseMap:
 *   new DynamicProperty( numberPropertyProperty, {
 *     bidirectional: true,
 *     map: function( number ) { return '' + number; },
 *     inverseMap: function( string ) { return Number.parseFloat( string ); }
 *   } );
 * so that changes to the dynamic Property will result in a change in the numberPropertyProperty's value.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
define( function( require ) {
  'use strict';

  // modules
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Property = require( 'AXON/Property' );

  /**
   * @constructor
   * @extends {Property}
   *
   * @param {Property.<*|null>} valuePropertyProperty - If the value is null, it is considered disconnected. If the
   *                                                    'derive' option is not used, then this should always have the
   *                                                    type {Property.<Property.<*>|null>}
   * @param {Object} [options] - options
   */
  function DynamicProperty( valuePropertyProperty, options ) {

    options = _.extend( {
      // {boolean} - If set to true then changes to this Property (if valuePropertyProperty.value is non-null at the
      //             time) will also be made to derive( valuePropertyProperty.value ).
      bidirectional: false,

      // {*} - If valuePropertyProperty.value === null, this dynamicProperty will act instead like
      //       derive( valuePropertyProperty.value ) === new Property( defaultValue ). Note that if a custom map
      //       function is provided, it will be applied to this defaultValue to determine our Property's value.
      defaultValue: null,

      // {function|string} - Maps a non-null valuePropertyProperty.value into the Property to be used. See top-level
      //                     documentation for usage. Uses the Lodash path specification (if it's a string).
      derive: _.identity,

      // {function|string} - Maps our input Property value to/from this Property's value. See top-level documentation
      //                     for usage. Uses the Lodash path specification (if it's a string).
      map: _.identity,
      inverseMap: _.identity
    }, options );

    // @public {boolean} - Set to true when this Property's value is changing from an external source.
    this.isExternallyChanging = false;

    // @private {Property.<*|null>}
    this.valuePropertyProperty = valuePropertyProperty;

    // @private {boolean}
    this.bidirectional = options.bidirectional;

    // @private {*}
    this.defaultValue = options.defaultValue;

    // @private {function}
    this.derive = typeof options.derive === 'string' ? _.property( options.derive ) : options.derive;
    this.map = typeof options.map === 'string' ? _.property( options.map ) : options.map;
    this.inverseMap = typeof options.inverseMap === 'string' ? _.property( options.inverseMap ) : options.inverseMap;

    // Use the Property's initial value
    var initialValue;
    if ( this.valuePropertyProperty.value === null ) {
      initialValue = this.map( this.defaultValue );
    }
    else {
      initialValue = this.map( this.derive( this.valuePropertyProperty.value ).value );
    }

    // Super call
    Property.call( this, initialValue, options );

    // @private {function}
    this.propertyPropertyListener = this.onPropertyPropertyChange.bind( this );

    // @private {function}
    this.propertyListener = this.onPropertyChange.bind( this );

    // Rehook our listener to whatever is the active Property.
    valuePropertyProperty.link( this.propertyListener );

    // If we aren't bidirectional, we should never add this listener.
    if ( options.bidirectional ) {
      this.lazyLink( this.onSelfChange.bind( this ) );
    }
  }

  axon.register( 'DynamicProperty', DynamicProperty );

  return inherit( Property, DynamicProperty, {
    /**
     * Listener added to the active inner Property.
     * @private
     *
     * @param {*} value - Should be either our defaultValue (if valuePropertyProperty.value is null), or
     *                    derive( valuePropertyProperty.value ).value otherwise.
     */
    onPropertyPropertyChange: function( value ) {
      // Since we override the setter here, we need to call the version on the prototype
      Property.prototype.set.call( this, this.map( value ) );
    },

    /**
     * Listener added to the outer Property.
     * @private
     *
     * @param {*|null} newPropertyValue - If derive is not provided then it should be a {Property.<*>|null}
     * @param {*|null|undefined} oldPropertyValue - If derive is not provided then it should be a {Property.<*>|null}.
     *                                              We additionally handle the initial link() case where this is
     *                                              undefined.
     */
    onPropertyChange: function( newPropertyValue, oldPropertyValue ) {
      if ( oldPropertyValue ) {
        this.derive( oldPropertyValue ).unlink( this.propertyPropertyListener );
      }
      if ( newPropertyValue ) {
        this.derive( newPropertyValue ).link( this.propertyPropertyListener );
      }
      else {
        // Switch to null when our Property's value is null.
        this.onPropertyPropertyChange( this.defaultValue );
      }
    },

    /**
     * Listener added to ourself when we are bidirectional
     * @private
     *
     * @param {*} value
     */
    onSelfChange: function( value ) {
      if ( this.valuePropertyProperty.value !== null ) {
        this.derive( this.valuePropertyProperty.value ).value = this.inverseMap( value );
      }
    },

    /**
     * Disposes this Property
     * @public
     */
    dispose: function() {
      this.valuePropertyProperty.unlink( this.propertyListener );

      if ( this.valuePropertyProperty.value !== null ) {
        this.derive( this.valuePropertyProperty.value ).unlink( this.propertyPropertyListener );
      }

      Property.prototype.dispose.call( this );
    },

    /**
     * Prevent setting this Property manually
     * @public
     * @override
     *
     * TODO: DerivedProperty should only need to do this, not all of the other methods it's also doing
     *
     * @param {*} value
     */
    set: function( value ) {
      assert && assert( this.bidirectional, 'Cannot set values directly to a DynamicProperty, tried to set: ' + value );

      this.isExternallyChanging = true;

      Property.prototype.set.call( this, value );

      this.isExternallyChanging = false;
    }
  } );
} );
<commit_msg>Fix docs for DynamicProperty set with bidirectional, and added reset forwarding.<commit_after>// Copyright 2017, University of Colorado Boulder

/**
 * Creates a Property that does synchronization of values with a swappable Property that itself can change.
 * Handles the case where you need a Property that can switch between acting like multiple other Properties.
 *
 * With no other options specified, the value of this Property is:
 * - null, if valuePropertyProperty.value === null
 * - valuePropertyProperty.value.value otherwise
 *
 * The value of this Property (generalized, with the options available) is:
 * - derive( defaultValue ), if valuePropertyProperty.value === null
 * - map( derive( valuePropertyProperty.value ).value ) otherwise
 *
 * Generally, this DynamicProperty uses one-way synchronization (it only listens to the source), but if the
 * 'bidirectional' option is true, it will use two-way synchronization (changes to this Property will change the active
 * source). Thus when this Property changes value (when bidirectional is true), it will set:
 * - derive( valuePropertyProperty.value ).value = inverseMap( this.value ), if valuePropertyProperty.value !== null
 *
 *******************************
 * General example
 *******************************
 *   var firstProperty = new Property( Color.RED );
 *   var secondProperty = new Property( Color.BLUE );
 *   var currentProperty = new Property( firstProperty ); // {Property.<Property.<Color>>}
 *
 *   var backgroundFill = new DynamicProperty( currentProperty ) // Turns into a {Property.<Color>}
 *   backgroundFill.value; // Color.RED, since: currentProperty.value === firstProperty and
 *                                              firstProperty.value === Color.RED
 *   firstProperty.value = Color.YELLOW;
 *   backgroundFill.value; // Color.YELLOW - It's connected to firstProperty right now
 *
 *   currentProperty.value = secondProperty;
 *   backgroundFill.value; // Color.BLUE - It's the secondProperty's value
 *
 *   secondProperty.value = Color.MAGENTA;
 *   backgroundFill.value; // Color.MAGENTA - Yes, it's listening to the other Property now.
 *
 * Also supports falling back to null if our main Property is set to null:
 *   currentProperty.value = null;
 *   backgroundFill.value; // null
 *
 *******************************
 * 'derive' option
 *******************************
 * Additionally, DynamicProperty supports the ability to derive the Property value from our main Property's value.
 * For example, say you have multiple scenes each with the type:
 *   scene: {
 *     backgroundColorProperty: {Property.<Color>}
 *   }
 * and you have a currentSceneProperty: {Property.<Scene>}, you may want to create:
 *   var currentBackgroundColorProperty = new DynamicProperty( currentSceneProperty, {
 *     derive: 'backgroundColorProperty'
 *   } );
 * This would always report the current scene's current background color.
 * What if you sometimes don't have a scene active, e.g. {Property.<Scene|null>}? You can provide a default value:
 *  new DynamicProperty( currentSceneProperty, {
 *    derive: 'backgroundColorProperty',
 *    defaultValue: Color.BLACK
 *  } );
 * So that if the currentSceneProperty's value is null, the value of our DynamicProperty will be Color.BLACK.
 *
 *******************************
 * 'bidirectional' option
 *******************************
 * If you would like for direct changes to this Property to change the original source (bidirectional synchronization),
 * then pass bidirectional:true:
 *   var firstProperty = new Property( 5 );
 *   var secondProperty = new Property( 10 );
 *   var numberPropertyProperty = new Property( firstProperty );
 *   var dynamicProperty = new DynamicProperty( numberPropertyProperty, { bidirectional: true } );
 *   dynamicProperty.value = 2; // allowed now that it is bidrectional, otherwise prohibited
 *   firstProperty.value; // 2
 *   numberPropertyProperty.value = secondProperty; // change which Property is active
 *   dynamicProperty.value; // 10, from the new Property
 *   dynamicProperty.value = 0;
 *   secondProperty.value; // 0, set above.
 *   firstProperty.value; // still 2 from above, since our dynamic Property switched to the other Property
 *
 *******************************
 * 'map' and 'inverseMap' options
 *******************************
 * DynamicProperty also supports mapping values to different types. For example, say we have a
 * numberPropertyProperty {Property.<Property.<number>>}, but want to have a {Property.<string>} as the output. Then:
 *   new DynamicProperty( numberPropertyProperty, {
 *     map: function( number ) { return '' + number; }
 *   } );
 * will do the trick. If this needs to be done with a bidirectional DynamicProperty, also include inverseMap:
 *   new DynamicProperty( numberPropertyProperty, {
 *     bidirectional: true,
 *     map: function( number ) { return '' + number; },
 *     inverseMap: function( string ) { return Number.parseFloat( string ); }
 *   } );
 * so that changes to the dynamic Property will result in a change in the numberPropertyProperty's value.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
define( function( require ) {
  'use strict';

  // modules
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Property = require( 'AXON/Property' );

  /**
   * @constructor
   * @extends {Property}
   *
   * @param {Property.<*|null>} valuePropertyProperty - If the value is null, it is considered disconnected. If the
   *                                                    'derive' option is not used, then this should always have the
   *                                                    type {Property.<Property.<*>|null>}
   * @param {Object} [options] - options
   */
  function DynamicProperty( valuePropertyProperty, options ) {

    options = _.extend( {
      // {boolean} - If set to true then changes to this Property (if valuePropertyProperty.value is non-null at the
      //             time) will also be made to derive( valuePropertyProperty.value ).
      bidirectional: false,

      // {*} - If valuePropertyProperty.value === null, this dynamicProperty will act instead like
      //       derive( valuePropertyProperty.value ) === new Property( defaultValue ). Note that if a custom map
      //       function is provided, it will be applied to this defaultValue to determine our Property's value.
      defaultValue: null,

      // {function|string} - Maps a non-null valuePropertyProperty.value into the Property to be used. See top-level
      //                     documentation for usage. Uses the Lodash path specification (if it's a string).
      derive: _.identity,

      // {function|string} - Maps our input Property value to/from this Property's value. See top-level documentation
      //                     for usage. Uses the Lodash path specification (if it's a string).
      map: _.identity,
      inverseMap: _.identity
    }, options );

    // @public {boolean} - Set to true when this Property's value is changing from an external source.
    this.isExternallyChanging = false;

    // @private {Property.<*|null>}
    this.valuePropertyProperty = valuePropertyProperty;

    // @private {boolean}
    this.bidirectional = options.bidirectional;

    // @private {*}
    this.defaultValue = options.defaultValue;

    // @private {function}
    this.derive = typeof options.derive === 'string' ? _.property( options.derive ) : options.derive;
    this.map = typeof options.map === 'string' ? _.property( options.map ) : options.map;
    this.inverseMap = typeof options.inverseMap === 'string' ? _.property( options.inverseMap ) : options.inverseMap;

    // Use the Property's initial value
    var initialValue;
    if ( this.valuePropertyProperty.value === null ) {
      initialValue = this.map( this.defaultValue );
    }
    else {
      initialValue = this.map( this.derive( this.valuePropertyProperty.value ).value );
    }

    // Super call
    Property.call( this, initialValue, options );

    // @private {function}
    this.propertyPropertyListener = this.onPropertyPropertyChange.bind( this );

    // @private {function}
    this.propertyListener = this.onPropertyChange.bind( this );

    // Rehook our listener to whatever is the active Property.
    valuePropertyProperty.link( this.propertyListener );

    // If we aren't bidirectional, we should never add this listener.
    if ( options.bidirectional ) {
      this.lazyLink( this.onSelfChange.bind( this ) );
    }
  }

  axon.register( 'DynamicProperty', DynamicProperty );

  return inherit( Property, DynamicProperty, {
    /**
     * Listener added to the active inner Property.
     * @private
     *
     * @param {*} value - Should be either our defaultValue (if valuePropertyProperty.value is null), or
     *                    derive( valuePropertyProperty.value ).value otherwise.
     */
    onPropertyPropertyChange: function( value ) {
      // Since we override the setter here, we need to call the version on the prototype
      Property.prototype.set.call( this, this.map( value ) );
    },

    /**
     * Listener added to the outer Property.
     * @private
     *
     * @param {*|null} newPropertyValue - If derive is not provided then it should be a {Property.<*>|null}
     * @param {*|null|undefined} oldPropertyValue - If derive is not provided then it should be a {Property.<*>|null}.
     *                                              We additionally handle the initial link() case where this is
     *                                              undefined.
     */
    onPropertyChange: function( newPropertyValue, oldPropertyValue ) {
      if ( oldPropertyValue ) {
        this.derive( oldPropertyValue ).unlink( this.propertyPropertyListener );
      }
      if ( newPropertyValue ) {
        this.derive( newPropertyValue ).link( this.propertyPropertyListener );
      }
      else {
        // Switch to null when our Property's value is null.
        this.onPropertyPropertyChange( this.defaultValue );
      }
    },

    /**
     * Listener added to ourself when we are bidirectional
     * @private
     *
     * @param {*} value
     */
    onSelfChange: function( value ) {
      if ( this.valuePropertyProperty.value !== null ) {
        this.derive( this.valuePropertyProperty.value ).value = this.inverseMap( value );
      }
    },

    /**
     * Disposes this Property
     * @public
     */
    dispose: function() {
      this.valuePropertyProperty.unlink( this.propertyListener );

      if ( this.valuePropertyProperty.value !== null ) {
        this.derive( this.valuePropertyProperty.value ).unlink( this.propertyPropertyListener );
      }

      Property.prototype.dispose.call( this );
    },

    /**
     * Resets the current property.
     * @public
     */
    reset: function() {
      assert && assert( this.bidirectional,
        'Cannot reset a non-bidirectional DynamicProperty' );

      if ( this.valuePropertyProperty.value !== null ) {
        this.valuePropertyProperty.value.reset();
      }
    },

    /**
     * Prevent setting this Property manually
     * @public
     * @override
     *
     * TODO: DerivedProperty should only need to do this, not all of the other methods it's also doing
     *
     * @param {*} value
     */
    set: function( value ) {
      assert && assert( this.bidirectional,
        'Cannot set values directly to a non-bidirectional DynamicProperty, tried to set: ' + value );

      this.isExternallyChanging = true;

      Property.prototype.set.call( this, value );

      this.isExternallyChanging = false;
    }
  } );
} );
<|endoftext|>


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

/**
 * An observable array of items.
 *
 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const inherit = require( 'PHET_CORE/inherit' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const ObjectIO = require( 'TANDEM/types/ObjectIO' );
  const ObservableArrayIO = require( 'AXON/ObservableArrayIO' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );

  // Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
  const DefaultObservableArrayIOType = ObservableArrayIO( ObjectIO );

  /**
   * @param {Object} [options]
   * @constructor
   */
  function ObservableArray( options ) {
    assert && assert( !Array.isArray( options ), 'ObservableArray cannot be initialized with values' );

    options = _.extend( {
      allowDuplicates: false, // are duplicate items allowed in the array?
      phetioType: DefaultObservableArrayIOType,
      tandem: Tandem.optional
    }, options );

    assert && assert( options.phetioType && options.phetioType.parameterType,
      'ObservableArray\'s phetioType should have a parameterType' );

    this.allowDuplicates = options.allowDuplicates; // @private

    this._options = options; // @private, for creating internal ObservableArray copies, see map/filter

    this._array = []; // @private internal, do not access directly
    this._addedListeners = []; // @private listeners called when an item is added
    this._removedListeners = []; // @private listeners called when an item is removed

    // @public (read-only) observe this, but don't set it
    this.lengthProperty = new NumberProperty( this._array.length, {
      numberType: 'Integer',
      tandem: options.tandem.createTandem( 'lengthProperty' ),
      phetioReadOnly: true
    } );

    PhetioObject.call( this, options );
  }

  axon.register( 'ObservableArray', ObservableArray );

  return inherit( PhetioObject, ObservableArray, {

    // @public
    dispose: function() {
      this.lengthProperty.dispose();
      PhetioObject.prototype.dispose.call( this );
    },

    /**
     * Restore the array back to its initial state
     * Note: if an item is in the current array and original array, it is removed and added back
     * This may or may not change in the future, see #4
     * @public
     */
    reset: function() {
      this.clear();
    },

    // @public
    get length() { return this._array.length; },

    /**
     * Adds a listener that will be notified when an item is added to the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemAddedListener: function( listener ) {
      assert && assert( this._addedListeners.indexOf( listener ) === -1 ); // listener is not already registered
      this._addedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemAddedListener.
     * @param listener
     * @public
     */
    removeItemAddedListener: function( listener ) {
      const index = this._addedListeners.indexOf( listener );
      assert && assert( index !== -1 ); // listener is registered
      this._addedListeners.splice( index, 1 );
    },

    /**
     * Adds a listener that will be notified when an item is removed from the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemRemovedListener: function( listener ) {
      assert && assert( this._removedListeners.indexOf( listener ) === -1, 'Listener was already registered' ); // listener is not already registered
      this._removedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemRemovedListener.
     * @param listener
     * @public
     */
    removeItemRemovedListener: function( listener ) {
      const index = this._removedListeners.indexOf( listener );
      assert && assert( index !== -1, 'Listener is still registered after removal' ); // listener is registered
      this._removedListeners.splice( index, 1 );
    },

    // @private called when an item is added.
    _fireItemAdded: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemAdded', function() {
        return self.phetioType.parameterType.toStateObject( item );
      } );

      //Signify that an item was added to the list
      const copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    // @private called when an item is removed.
    _fireItemRemoved: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemRemoved', function() {
        return self.phetioType.parameterType.toStateObject( item );
      } );

      //Signify that an item was removed from the list
      const copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    /**
     * Adds an item to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param item
     * @public
     */
    add: function( item ) {
      this.push( item );
    },

    /**
     * Add items to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param {Array} items
     * @public
     */
    addAll: function( items ) {
      for ( let i = 0; i < items.length; i++ ) {
        this.add( items[ i ] );
      }
    },

    /**
     * Removes the first occurrence of an item from the array.
     * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
     * times to totally purge item from the array.
     * @param item
     * @public
     */
    remove: function( item ) {
      const index = this._array.indexOf( item );
      if ( index !== -1 ) {
        this._array.splice( index, 1 );
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
    },

    /**
     * Removes the first occurrence of each item in the specified array.
     * @param {Array} array a list of items to remove
     * @see ObservableArray.remove
     * @public
     */
    removeAll: function( array ) {
      assert && assert( _.isArray( array ), 'array should be an array' );
      for ( let i = 0; i < array.length; i++ ) {
        this.remove( array[ i ] );
      }
    },

    /**
     * Pushes an item onto the end of the array.
     * @param item
     * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
     * @public
     */
    push: function( item ) {
      if ( !this.allowDuplicates && this.contains( item ) ) {
        throw new Error( 'duplicates are not allowed' );
      }
      this._array.push( item );
      this.lengthProperty.set( this._array.length );
      this._fireItemAdded( item );
    },

    /**
     * Removes an item from the end of the array and returns it.
     * @returns {*}
     * @public
     */
    pop: function() {

      // TODO: should we really pop if the length was 0?  Or maybe this is OK?
      const item = this._array.pop();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Removes an item from the beginning of the array and returns it.
     * @returns {*}
     * @public
     */
    shift: function() {
      const item = this._array.shift();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Does the array contain the specified item?
     * @param item
     * @returns {boolean}
     * @public
     */
    contains: function( item ) {
      return this.indexOf( item ) !== -1;
    },

    /**
     * Gets an item at the specified index.
     * @param index
     * @returns {*} the item, or undefined if there is no item at the specified index
     * @public
     */
    get: function( index ) {
      return this._array[ index ];
    },

    /**
     * Gets the index of a specified item.
     * @param item
     * @returns {*} -1 if item is not in the array
     * @public
     */
    indexOf: function( item ) {
      return this._array.indexOf( item );
    },

    /**
     * Removes all items from the array.
     * @public
     */
    clear: function() {
      while ( this.length > 0 ) {
        this.pop();
      }
    },

    /**
     * Applies a callback function to each item in the array
     * @param callback function(item)
     * @public
     */
    forEach: function( callback ) {
      this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
    },

    /**
     * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
     * @param {function(element:*):*} mapFunction - from Observable array element to something else
     * @returns {ObservableArray}
     * @public
     */
    map: function( mapFunction ) {
      const observableArray = new ObservableArray( this._options );
      this._array.map( mapFunction ).forEach( item => observableArray.push( item ) );
      return observableArray;
    },

    /**
     * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
     * @param {function(element):boolean} predicate
     * @returns {ObservableArray}
     * @public
     */
    filter: function( predicate ) {
      const observableArray = new ObservableArray( this._options );
      this._array.filter( predicate ).forEach( item => observableArray.push( item ) );
      return observableArray;
    },

    /**
     * Count the number of items in this ObservableArray that satisfy the given Predicate.
     * @param {function} predicate
     * @returns {number}
     * @public
     */
    count: function( predicate ) {
      let count = 0;
      for ( let i = 0; i < this._array.length; i++ ) {
        if ( predicate( this._array[ i ] ) ) {
          count++;
        }
      }
      return count;
    },

    /**
     * Find the first element that matches the given predicate.
     * @param {function} predicate
     * @param {number} [fromIndex] - optional start index for the search
     */
    find: function( predicate, fromIndex ) {
      return _.find( this._array, predicate, fromIndex );
    },

    /**
     * Returns true if some element in this ObservableArray matches the predicate.
     * @param {function} predicate - the function to test elements
     * @returns {boolean}
     */
    some: function( predicate ) {
      return _.some( this._array, predicate );
    },

    /**
     * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
     * Same as foldLeft.  In underscore this is called _.reduce aka _.fold or _.inject
     * @param value
     * @param combiner
     * @returns {*}
     * @public
     */
    reduce: function( value, combiner ) {
      for ( let i = 0; i < this._array.length; i++ ) {
        value = combiner( value, this._array[ i ] );
      }
      return value;
    },

    /**
     * Return the underlying array
     * @returns {Array}
     * @public
     */
    getArray: function() {
      return this._array;
    },

    /**
     * Add/remove elements from any point in the array
     * @param {number} start - the index to start adding/removing items
     * @param {number} deleteCount - the number of items to delete
     * @param {Object} [item1] - an item to add
     * @param {Object} [item2] - an item to add
     * @param {Object} [etc] - varargs items to add etc.
     * @returns {Object[]} the items that were deleted.
     * @public
     */
    splice: function( start, deleteCount, item1, item2, etc ) {
      const deleted = this._array.splice.apply( this._array, arguments );
      const args = Array.prototype.slice.call( arguments );
      for ( let i = 0; i < deleted.length; i++ ) {
        this._fireItemRemoved( deleted[ i ] );
      }

      for ( let k = 2; k < args.length; k++ ) {
        this._fireItemAdded( args[ k ] );
      }
      return deleted;
    },

    /**
     * Changes the ordering of elements in the array.  Requires a Random source so that shuffles can be reproducible.
     * No items are added or removed, and this method does not send out any notifications.
     * @param {Random} random - from dot
     */
    shuffle: function( random ) {
      assert && assert( random, 'random must be supplied' );

      // preserve the same _array reference in case any clients got a reference to it with getArray()
      const shuffled = random.shuffle( this._array );
      this._array.length = 0;
      Array.prototype.push.apply( this._array, shuffled );
    }
  } );
} );<commit_msg>factor out view code into ElectricPotentialLineView, make potential line view and model more mutable, https://github.com/phetsims/charges-and-fields/issues/170<commit_after>// Copyright 2013-2019, University of Colorado Boulder

/**
 * An observable array of items.
 *
 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const inherit = require( 'PHET_CORE/inherit' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const ObjectIO = require( 'TANDEM/types/ObjectIO' );
  const ObservableArrayIO = require( 'AXON/ObservableArrayIO' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );

  // Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
  const DefaultObservableArrayIOType = ObservableArrayIO( ObjectIO );

  /**
   * @param {Object} [options]
   * @constructor
   */
  function ObservableArray( options ) {
    assert && assert( !Array.isArray( options ), 'ObservableArray cannot be initialized with values' );

    options = _.extend( {
      allowDuplicates: false, // are duplicate items allowed in the array?
      phetioType: DefaultObservableArrayIOType,
      tandem: Tandem.optional
    }, options );

    assert && assert( options.phetioType && options.phetioType.parameterType,
      'ObservableArray\'s phetioType should have a parameterType' );

    this.allowDuplicates = options.allowDuplicates; // @private

    this._options = options; // @private, for creating internal ObservableArray copies, see map/filter

    this._array = []; // @private internal, do not access directly
    this._addedListeners = []; // @private listeners called when an item is added
    this._removedListeners = []; // @private listeners called when an item is removed

    // @public (read-only) observe this, but don't set it
    this.lengthProperty = new NumberProperty( this._array.length, {
      numberType: 'Integer',
      tandem: options.tandem.createTandem( 'lengthProperty' ),
      phetioReadOnly: true
    } );

    PhetioObject.call( this, options );
  }

  axon.register( 'ObservableArray', ObservableArray );

  return inherit( PhetioObject, ObservableArray, {

    // @public
    dispose: function() {
      this.lengthProperty.dispose();
      PhetioObject.prototype.dispose.call( this );
    },

    /**
     * Restore the array back to its initial state
     * Note: if an item is in the current array and original array, it is removed and added back
     * This may or may not change in the future, see #4
     * @public
     */
    reset: function() {
      this.clear();
    },

    // @public
    get length() { return this._array.length; },

    /**
     * Adds a listener that will be notified when an item is added to the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemAddedListener: function( listener ) {
      assert && assert( this._addedListeners.indexOf( listener ) === -1 ); // listener is not already registered
      this._addedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemAddedListener.
     * @param listener
     * @public
     */
    removeItemAddedListener: function( listener ) {
      const index = this._addedListeners.indexOf( listener );
      assert && assert( index !== -1, 'Listener is not found in item added listeners' ); // listener is registered
      this._addedListeners.splice( index, 1 );
    },

    /**
     * Adds a listener that will be notified when an item is removed from the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemRemovedListener: function( listener ) {
      assert && assert( this._removedListeners.indexOf( listener ) === -1, 'Listener was already registered' ); // listener is not already registered
      this._removedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemRemovedListener.
     * @param listener
     * @public
     */
    removeItemRemovedListener: function( listener ) {
      const index = this._removedListeners.indexOf( listener );
      assert && assert( index !== -1, 'Listener is not found in item removed listeners' ); // listener is registered
      this._removedListeners.splice( index, 1 );
    },

    // @private called when an item is added.
    _fireItemAdded: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemAdded', function() {
        return self.phetioType.parameterType.toStateObject( item );
      } );

      //Signify that an item was added to the list
      const copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    // @private called when an item is removed.
    _fireItemRemoved: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemRemoved', function() {
        return self.phetioType.parameterType.toStateObject( item );
      } );

      //Signify that an item was removed from the list
      const copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    /**
     * Adds an item to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param item
     * @public
     */
    add: function( item ) {
      this.push( item );
    },

    /**
     * Add items to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param {Array} items
     * @public
     */
    addAll: function( items ) {
      for ( let i = 0; i < items.length; i++ ) {
        this.add( items[ i ] );
      }
    },

    /**
     * Removes the first occurrence of an item from the array.
     * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
     * times to totally purge item from the array.
     * @param item
     * @public
     */
    remove: function( item ) {
      const index = this._array.indexOf( item );
      if ( index !== -1 ) {
        this._array.splice( index, 1 );
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
    },

    /**
     * Removes the first occurrence of each item in the specified array.
     * @param {Array} array a list of items to remove
     * @see ObservableArray.remove
     * @public
     */
    removeAll: function( array ) {
      assert && assert( _.isArray( array ), 'array should be an array' );
      for ( let i = 0; i < array.length; i++ ) {
        this.remove( array[ i ] );
      }
    },

    /**
     * Pushes an item onto the end of the array.
     * @param item
     * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
     * @public
     */
    push: function( item ) {
      if ( !this.allowDuplicates && this.contains( item ) ) {
        throw new Error( 'duplicates are not allowed' );
      }
      this._array.push( item );
      this.lengthProperty.set( this._array.length );
      this._fireItemAdded( item );
    },

    /**
     * Removes an item from the end of the array and returns it.
     * @returns {*}
     * @public
     */
    pop: function() {

      // TODO: should we really pop if the length was 0?  Or maybe this is OK?
      const item = this._array.pop();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Removes an item from the beginning of the array and returns it.
     * @returns {*}
     * @public
     */
    shift: function() {
      const item = this._array.shift();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Does the array contain the specified item?
     * @param item
     * @returns {boolean}
     * @public
     */
    contains: function( item ) {
      return this.indexOf( item ) !== -1;
    },

    /**
     * Gets an item at the specified index.
     * @param index
     * @returns {*} the item, or undefined if there is no item at the specified index
     * @public
     */
    get: function( index ) {
      return this._array[ index ];
    },

    /**
     * Gets the index of a specified item.
     * @param item
     * @returns {*} -1 if item is not in the array
     * @public
     */
    indexOf: function( item ) {
      return this._array.indexOf( item );
    },

    /**
     * Removes all items from the array.
     * @public
     */
    clear: function() {
      while ( this.length > 0 ) {
        this.pop();
      }
    },

    /**
     * Applies a callback function to each item in the array
     * @param callback function(item)
     * @public
     */
    forEach: function( callback ) {
      this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
    },

    /**
     * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
     * @param {function(element:*):*} mapFunction - from Observable array element to something else
     * @returns {ObservableArray}
     * @public
     */
    map: function( mapFunction ) {
      const observableArray = new ObservableArray( this._options );
      this._array.map( mapFunction ).forEach( item => observableArray.push( item ) );
      return observableArray;
    },

    /**
     * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
     * @param {function(element):boolean} predicate
     * @returns {ObservableArray}
     * @public
     */
    filter: function( predicate ) {
      const observableArray = new ObservableArray( this._options );
      this._array.filter( predicate ).forEach( item => observableArray.push( item ) );
      return observableArray;
    },

    /**
     * Count the number of items in this ObservableArray that satisfy the given Predicate.
     * @param {function} predicate
     * @returns {number}
     * @public
     */
    count: function( predicate ) {
      let count = 0;
      for ( let i = 0; i < this._array.length; i++ ) {
        if ( predicate( this._array[ i ] ) ) {
          count++;
        }
      }
      return count;
    },

    /**
     * Find the first element that matches the given predicate.
     * @param {function} predicate
     * @param {number} [fromIndex] - optional start index for the search
     */
    find: function( predicate, fromIndex ) {
      return _.find( this._array, predicate, fromIndex );
    },

    /**
     * Returns true if some element in this ObservableArray matches the predicate.
     * @param {function} predicate - the function to test elements
     * @returns {boolean}
     */
    some: function( predicate ) {
      return _.some( this._array, predicate );
    },

    /**
     * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
     * Same as foldLeft.  In underscore this is called _.reduce aka _.fold or _.inject
     * @param value
     * @param combiner
     * @returns {*}
     * @public
     */
    reduce: function( value, combiner ) {
      for ( let i = 0; i < this._array.length; i++ ) {
        value = combiner( value, this._array[ i ] );
      }
      return value;
    },

    /**
     * Return the underlying array
     * @returns {Array}
     * @public
     */
    getArray: function() {
      return this._array;
    },

    /**
     * Add/remove elements from any point in the array
     * @param {number} start - the index to start adding/removing items
     * @param {number} deleteCount - the number of items to delete
     * @param {Object} [item1] - an item to add
     * @param {Object} [item2] - an item to add
     * @param {Object} [etc] - varargs items to add etc.
     * @returns {Object[]} the items that were deleted.
     * @public
     */
    splice: function( start, deleteCount, item1, item2, etc ) {
      const deleted = this._array.splice.apply( this._array, arguments );
      const args = Array.prototype.slice.call( arguments );
      for ( let i = 0; i < deleted.length; i++ ) {
        this._fireItemRemoved( deleted[ i ] );
      }

      for ( let k = 2; k < args.length; k++ ) {
        this._fireItemAdded( args[ k ] );
      }
      return deleted;
    },

    /**
     * Changes the ordering of elements in the array.  Requires a Random source so that shuffles can be reproducible.
     * No items are added or removed, and this method does not send out any notifications.
     * @param {Random} random - from dot
     */
    shuffle: function( random ) {
      assert && assert( random, 'random must be supplied' );

      // preserve the same _array reference in case any clients got a reference to it with getArray()
      const shuffled = random.shuffle( this._array );
      this._array.length = 0;
      Array.prototype.push.apply( this._array, shuffled );
    }
  } );
} );<|endoftext|>


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

/**
 * An observable array of items.
 *

 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid
 * @author Chris Malley
 */
define( function( require ) {
  'use strict';

  // modules
  var Property = require( 'AXON/Property' );
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Emitter = require( 'AXON/Emitter' );

  /**
   * @param {[]} array
   * @param {Object} [options]
   * @constructor
   */
  function ObservableArray( array, options ) {

    // Special case that the user supplied options but no array
    if ( array instanceof Object && !(array instanceof Array) ) {
      options = array;
      array = null;
    }

    this._options = _.extend( {
      allowDuplicates: false, // are duplicate items allowed in the array?
      tandem: null            // Tandem is supported here.  This line doesn't do anything different than leaving tandem as undefined
                              // but this entry serves as an indicator that tandem is supported here.
    }, options );

    this._array = array || []; // @private internal, do not access directly
    this._addedListeners = []; // @private listeners called when an item is added
    this._removedListeners = []; // @private listeners called when an item is removed

    this.lengthProperty = new Property( this._array.length ); // @public (read-only) observe this, but don't set it

    // @private Store the initial array, if any, for resetting, see #4
    this.initialArray = array ? array.slice() : [];

    // @private Event stream for signifying begin/end of callbacks
    this.startedCallbacksForItemAddedEmitter = new Emitter();
    this.endedCallbacksForItemAddedEmitter = new Emitter();
    this.startedCallbacksForItemRemovedEmitter = new Emitter();
    this.endedCallbacksForItemRemovedEmitter = new Emitter();

    options && options.tandem && options.tandem.addInstance( this );
    this.disposeObservableArray = function() {
      options && options.tandem && options.tandem.removeInstance( this );
    };
  }

  axon.register( 'ObservableArray', ObservableArray );

  return inherit( Object, ObservableArray, {

    // @public
    dispose: function() {
      this.disposeObservableArray();
    },

    /**
     * Restore the array back to its initial state
     * Note: if an item is in the current array and original array, it is removed and added back
     * This may or may not change in the future, see #4
     * @public
     */
    reset: function() {
      for ( var i = 0; i < this._array.length; i++ ) {
        this._fireItemRemoved( this._array[ i ] );
      }
      this._array = this.initialArray.slice();
      for ( i = 0; i < this._array.length; i++ ) {
        this._fireItemAdded( this._array[ i ] );
      }
    },

    // @public
    get length() { return this._array.length; },

    /**
     * Adds a listener that will be notified when an item is added to the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemAddedListener: function( listener ) {
      assert && assert( this._addedListeners.indexOf( listener ) === -1 ); // listener is not already registered
      this._addedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemAddedListener.
     * @param listener
     * @public
     */
    removeItemAddedListener: function( listener ) {
      var index = this._addedListeners.indexOf( listener );
      assert && assert( index !== -1 ); // listener is registered
      this._addedListeners.splice( index, 1 );
    },

    /**
     * Adds a listener that will be notified when an item is removed from the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemRemovedListener: function( listener ) {
      assert && assert( this._removedListeners.indexOf( listener ) === -1, 'Listener was already registered' ); // listener is not already registered
      this._removedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemRemovedListener.
     * @param listener
     * @public
     */
    removeItemRemovedListener: function( listener ) {
      var index = this._removedListeners.indexOf( listener );
      assert && assert( index !== -1, 'Listener is still registered after removal' ); // listener is registered
      this._removedListeners.splice( index, 1 );
    },

    /**
     * Convenience function for adding both types of listeners in one shot.
     * @param itemAddedListener
     * @param itemRemovedListener
     * @public
     */
    addListeners: function( itemAddedListener, itemRemovedListener ) {
      this.addItemAddedListener( itemAddedListener );
      this.addItemRemovedListener( itemRemovedListener );
    },

    // @private Internal: called when an item is added.
    _fireItemAdded: function( item ) {
      this.startedCallbacksForItemAddedEmitter.emit1( item );

      //Signify that an item was added to the list
      var copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( var i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.endedCallbacksForItemAddedEmitter.emit();
    },

    // Internal: called when an item is removed.
    _fireItemRemoved: function( item ) {

      this.startedCallbacksForItemRemovedEmitter.emit1( item );

      //Signify that an item was removed from the list
      var copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( var i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.endedCallbacksForItemRemovedEmitter.emit();
    },

    /**
     * Adds an item to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param item
     * @public
     */
    add: function( item ) {
      this.push( item );
    },

    /**
     * Add items to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param {Array} items
     * @public
     */
    addAll: function( items ) {
      for ( var i = 0; i < items.length; i++ ) {
        this.add( items[ i ] );
      }
    },

    /**
     * Removes the first occurrence of an item from the array.
     * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
     * times to totally purge item from the array.
     * @param item
     * @public
     */
    remove: function( item ) {
      var index = this._array.indexOf( item );
      if ( index !== -1 ) {
        this._array.splice( index, 1 );
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
    },

    /**
     * Removes the first occurrence of each item in the specified array.
     * @param {Array} list a list of items to remove
     * @see ObservableArray.remove
     * @public
     */
    removeAll: function( list ) {
      for ( var i = 0; i < list.length; i++ ) {
        var item = list[ i ];
        this.remove( item );
      }
    },

    /**
     * Pushes an item onto the end of the array.
     * @param item
     * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
     * @public
     */
    push: function( item ) {
      if ( !this._options.allowDuplicates && this.contains( item ) ) {
        throw new Error( 'duplicates are not allowed' );
      }
      this._array.push( item );
      this.lengthProperty.set( this._array.length );
      this._fireItemAdded( item );
    },

    /**
     * Removes an item from the end of the array and returns it.
     * @returns {*}
     * @public
     */
    pop: function() {
      var item = this._array.pop();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Removes an item from the beginning of the array and returns it.
     * @returns {*}
     * @public
     */
    shift: function() {
      var item = this._array.shift();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Does the array contain the specified item?
     * @param item
     * @returns {boolean}
     * @public
     */
    contains: function( item ) {
      return this.indexOf( item ) !== -1;
    },

    /**
     * Gets an item at the specified index.
     * @param index
     * @returns {*} the item, or undefined if there is no item at the specified index
     * @public
     */
    get: function( index ) {
      return this._array[ index ];
    },

    /**
     * Gets the index of a specified item.
     * @param item
     * @returns {*} -1 if item is not in the array
     * @public
     */
    indexOf: function( item ) {
      return this._array.indexOf( item );
    },

    /**
     * Removes all items from the array.
     * @public
     */
    clear: function() {
      var copy = this._array.slice( 0 );
      for ( var i = 0; i < copy.length; i++ ) {
        this.remove( copy[ i ] );
      }
    },

    /**
     * Applies a callback function to each item in the array
     * @param callback function(item)
     * @public
     */
    forEach: function( callback ) {
      this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
    },

    /**
     * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
     * @param mapFunction
     * @returns {axon.ObservableArray}
     * @public
     */
    map: function( mapFunction ) {
      return new axon.ObservableArray( this._array.map( mapFunction ) );
    },

    /**
     * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
     * @param predicate
     * @returns {axon.ObservableArray}
     * @public
     */
    filter: function( predicate ) {
      return new axon.ObservableArray( this._array.filter( predicate ) );
    },

    /**
     * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
     * Same as foldLeft.  In underscore this is called _.reduce aka _.foldl or _.inject
     * @param value
     * @param combiner
     * @returns {*}
     * @public
     */
    reduce: function( value, combiner ) {
      for ( var i = 0; i < this._array.length; i++ ) {
        value = combiner( value, this._array[ i ] );
      }
      return value;
    },

    /**
     * Return the underlying array
     * @returns {*|Array}
     * @public
     */
    getArray: function() {
      return this._array;
    }
  } );
} );<commit_msg>Cleaned up types<commit_after>// Copyright 2013-2015, University of Colorado Boulder

/**
 * An observable array of items.
 *

 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid
 * @author Chris Malley
 */
define( function( require ) {
  'use strict';

  // modules
  var Property = require( 'AXON/Property' );
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Emitter = require( 'AXON/Emitter' );

  /**
   * @param {[]} array
   * @param {Object} [options]
   * @constructor
   */
  function ObservableArray( array, options ) {

    // Special case that the user supplied options but no array
    if ( array instanceof Object && !(array instanceof Array) ) {
      options = array;
      array = null;
    }

    this._options = _.extend( {
      allowDuplicates: false, // are duplicate items allowed in the array?
      tandem: null            // Tandem is supported here.  This line doesn't do anything different than leaving tandem as undefined
                              // but this entry serves as an indicator that tandem is supported here.
    }, options );

    this._array = array || []; // @private internal, do not access directly
    this._addedListeners = []; // @private listeners called when an item is added
    this._removedListeners = []; // @private listeners called when an item is removed

    this.lengthProperty = new Property( this._array.length ); // @public (read-only) observe this, but don't set it

    // @private Store the initial array, if any, for resetting, see #4
    this.initialArray = array ? array.slice() : [];

    // @private Event stream for signifying begin/end of callbacks
    this.startedCallbacksForItemAddedEmitter = new Emitter();
    this.endedCallbacksForItemAddedEmitter = new Emitter();
    this.startedCallbacksForItemRemovedEmitter = new Emitter();
    this.endedCallbacksForItemRemovedEmitter = new Emitter();

    options && options.tandem && options.tandem.addInstance( this );
    this.disposeObservableArray = function() {
      options && options.tandem && options.tandem.removeInstance( this );
    };
  }

  axon.register( 'ObservableArray', ObservableArray );

  return inherit( Object, ObservableArray, {

    // @public
    dispose: function() {
      this.disposeObservableArray();
    },

    /**
     * Restore the array back to its initial state
     * Note: if an item is in the current array and original array, it is removed and added back
     * This may or may not change in the future, see #4
     * @public
     */
    reset: function() {
      for ( var i = 0; i < this._array.length; i++ ) {
        this._fireItemRemoved( this._array[ i ] );
      }
      this._array = this.initialArray.slice();
      for ( i = 0; i < this._array.length; i++ ) {
        this._fireItemAdded( this._array[ i ] );
      }
    },

    // @public
    get length() { return this._array.length; },

    /**
     * Adds a listener that will be notified when an item is added to the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemAddedListener: function( listener ) {
      assert && assert( this._addedListeners.indexOf( listener ) === -1 ); // listener is not already registered
      this._addedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemAddedListener.
     * @param listener
     * @public
     */
    removeItemAddedListener: function( listener ) {
      var index = this._addedListeners.indexOf( listener );
      assert && assert( index !== -1 ); // listener is registered
      this._addedListeners.splice( index, 1 );
    },

    /**
     * Adds a listener that will be notified when an item is removed from the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemRemovedListener: function( listener ) {
      assert && assert( this._removedListeners.indexOf( listener ) === -1, 'Listener was already registered' ); // listener is not already registered
      this._removedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemRemovedListener.
     * @param listener
     * @public
     */
    removeItemRemovedListener: function( listener ) {
      var index = this._removedListeners.indexOf( listener );
      assert && assert( index !== -1, 'Listener is still registered after removal' ); // listener is registered
      this._removedListeners.splice( index, 1 );
    },

    /**
     * Convenience function for adding both types of listeners in one shot.
     * @param itemAddedListener
     * @param itemRemovedListener
     * @public
     */
    addListeners: function( itemAddedListener, itemRemovedListener ) {
      this.addItemAddedListener( itemAddedListener );
      this.addItemRemovedListener( itemRemovedListener );
    },

    // @private Internal: called when an item is added.
    _fireItemAdded: function( item ) {
      this.startedCallbacksForItemAddedEmitter.emit1( item );

      //Signify that an item was added to the list
      var copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( var i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.endedCallbacksForItemAddedEmitter.emit();
    },

    // Internal: called when an item is removed.
    _fireItemRemoved: function( item ) {

      this.startedCallbacksForItemRemovedEmitter.emit1( item );

      //Signify that an item was removed from the list
      var copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( var i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.endedCallbacksForItemRemovedEmitter.emit();
    },

    /**
     * Adds an item to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param item
     * @public
     */
    add: function( item ) {
      this.push( item );
    },

    /**
     * Add items to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param {Array} items
     * @public
     */
    addAll: function( items ) {
      for ( var i = 0; i < items.length; i++ ) {
        this.add( items[ i ] );
      }
    },

    /**
     * Removes the first occurrence of an item from the array.
     * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
     * times to totally purge item from the array.
     * @param item
     * @public
     */
    remove: function( item ) {
      var index = this._array.indexOf( item );
      if ( index !== -1 ) {
        this._array.splice( index, 1 );
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
    },

    /**
     * Removes the first occurrence of each item in the specified array.
     * @param {Array} array a list of items to remove
     * @see ObservableArray.remove
     * @public
     */
    removeAll: function( array ) {
      assert && assert( _.isArray( array ), 'array should be an array' );
      for ( var i = 0; i < array.length; i++ ) {
        this.remove( array[ i ] );
      }
    },

    /**
     * Pushes an item onto the end of the array.
     * @param item
     * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
     * @public
     */
    push: function( item ) {
      if ( !this._options.allowDuplicates && this.contains( item ) ) {
        throw new Error( 'duplicates are not allowed' );
      }
      this._array.push( item );
      this.lengthProperty.set( this._array.length );
      this._fireItemAdded( item );
    },

    /**
     * Removes an item from the end of the array and returns it.
     * @returns {*}
     * @public
     */
    pop: function() {
      var item = this._array.pop();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Removes an item from the beginning of the array and returns it.
     * @returns {*}
     * @public
     */
    shift: function() {
      var item = this._array.shift();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Does the array contain the specified item?
     * @param item
     * @returns {boolean}
     * @public
     */
    contains: function( item ) {
      return this.indexOf( item ) !== -1;
    },

    /**
     * Gets an item at the specified index.
     * @param index
     * @returns {*} the item, or undefined if there is no item at the specified index
     * @public
     */
    get: function( index ) {
      return this._array[ index ];
    },

    /**
     * Gets the index of a specified item.
     * @param item
     * @returns {*} -1 if item is not in the array
     * @public
     */
    indexOf: function( item ) {
      return this._array.indexOf( item );
    },

    /**
     * Removes all items from the array.
     * @public
     */
    clear: function() {
      var copy = this._array.slice( 0 );
      for ( var i = 0; i < copy.length; i++ ) {
        this.remove( copy[ i ] );
      }
    },

    /**
     * Applies a callback function to each item in the array
     * @param callback function(item)
     * @public
     */
    forEach: function( callback ) {
      this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
    },

    /**
     * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
     * @param mapFunction
     * @returns {ObservableArray}
     * @public
     */
    map: function( mapFunction ) {
      return new ObservableArray( this._array.map( mapFunction ) );
    },

    /**
     * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
     * @param predicate
     * @returns {ObservableArray}
     * @public
     */
    filter: function( predicate ) {
      return new ObservableArray( this._array.filter( predicate ) );
    },

    /**
     * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
     * Same as foldLeft.  In underscore this is called _.reduce aka _.foldl or _.inject
     * @param value
     * @param combiner
     * @returns {*}
     * @public
     */
    reduce: function( value, combiner ) {
      for ( var i = 0; i < this._array.length; i++ ) {
        value = combiner( value, this._array[ i ] );
      }
      return value;
    },

    /**
     * Return the underlying array
     * @returns {*|Array}
     * @public
     */
    getArray: function() {
      return this._array;
    }
  } );
} );<|endoftext|>


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

/**
 * Thermometer node, see https://github.com/phetsims/scenery-phet/issues/43
 *
 * @author Aaron Davis
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var ShadedSphereNode = require( 'SCENERY_PHET/ShadedSphereNode' );
  var Shape = require( 'KITE/Shape' );

  // constants
  var FLUID_OVERLAP = 1; // overlap of fluid in tube and bulb, to hide seam
  // center of the bulb is at (0,0), let the client code move to the correct position
  var BULB_CENTER_X = 0;
  var BULB_CENTER_Y = 0;

  /**
   * @param {number} minTemperature
   * @param {number} maxTemperature
   * @param {Property.<number>} temperatureProperty
   * @param {Object} [options]
   * @constructor
   */
  function ThermometerNode( minTemperature, maxTemperature, temperatureProperty, options ) {
    options = _.extend( {
      bulbDiameter: 50,
      tubeWidth: 30,
      tubeHeight: 100,
      lineWidth: 4,
      outlineStroke: 'black',
      tickSpacing: 15,
      majorTickLength: 15,
      minorTickLength: 7.5,
      glassThickness: 2, // space between the thermometer outline and the fluid inside it

      // leave as null to have a transparent background. If a color is given, then an extra Rectangle is created for the background
      backgroundFill: null,

      // all the default colors are shades of red
      fluidMainColor: '#850e0e', // the main color of the bulb fluid, and the left side of the tube gradient
      fluidHighlightColor: '#ff7575', // the highlight color of the bulb fluid and the middle of the tube gradient
      fluidRightSideColor: '#c41515' // the right side of the tube gradient, not used currently
    }, options );

    Node.call( this );

    // Create a shaded sphere to act as the bulb fluid
    var bulbFluidDiameter = options.bulbDiameter - options.lineWidth - options.glassThickness; //TODO should this be options.lineWidth/2 ?
    var bulbFluidNode = new ShadedSphereNode( bulbFluidDiameter, {
      centerX: BULB_CENTER_X,
      centerY: BULB_CENTER_Y,
      mainColor: options.fluidMainColor,
      highlightColor: options.fluidHighlightColor,
      highlightXOffset: -0.2,
      highlightYOffset: 0.2,
      rotation: Math.PI / 2
    } );

    // Angles for the outline of the bulb
    var bulbStartAngle = -Math.acos( options.tubeWidth / options.bulbDiameter );
    var bulbEndAngle = Math.PI - bulbStartAngle;

    // Create the outline for the thermometer, starting with the bulb
    var tubeTopRadius = options.tubeWidth / 2;
    var straightTubeHeight = options.tubeHeight - tubeTopRadius;
    var straightTubeTop = BULB_CENTER_Y - ( options.bulbDiameter / 2 ) - straightTubeHeight;
    var straightTubeLeft = BULB_CENTER_X - ( options.tubeWidth / 2 );

    var outlineShape = new Shape()
      .arc( BULB_CENTER_X, BULB_CENTER_Y, options.bulbDiameter / 2, bulbStartAngle, bulbEndAngle ) // bulb at bottom
      .arc( BULB_CENTER_X, straightTubeTop, tubeTopRadius, Math.PI, 0 ) // rounded top of tube
      .close();
    // tick marks, from top down, alternating major and minor ticks
    var numberOfTicks = Math.floor( straightTubeHeight / options.tickSpacing ) + 1;
    for ( var i = 0; i < numberOfTicks; i++ ) {
      outlineShape.moveTo( straightTubeLeft, straightTubeTop + ( i * options.tickSpacing ) );
      outlineShape.horizontalLineTo( straightTubeLeft + ( ( i % 2 === 0 ) ? options.majorTickLength : options.minorTickLength ) );
    }

    var outlineNode = new Path( outlineShape, {
      stroke: options.outlineStroke,
      lineWidth: options.lineWidth
    } );
    assert && assert( outlineNode.height === options.tubeHeight + options.bulbDiameter + options.lineWidth ); // see scenery-phet#136

    var tubeFluidWidth = options.tubeWidth - options.lineWidth - options.glassThickness; //TODO should this be options.lineWidth/2 ?
    var tubeFluidRadius = tubeFluidWidth / 2;
    var clipBulbRadius = ( options.bulbDiameter - options.lineWidth - options.glassThickness ) / 2; //TODO should this be options.lineWidth/2 ?
    var clipStartAngle = -Math.acos( tubeFluidRadius / clipBulbRadius );
    var clipEndAngle = Math.PI - clipStartAngle;
    var tubeFluidBottom = ( bulbFluidDiameter / 2 ) * Math.sin( clipEndAngle );
    var tubeFluidLeft = -tubeFluidRadius;

    // Clip area for the fluid in the tube, round at the top
    var fluidClipArea = new Shape()
      .moveTo( tubeFluidLeft, tubeFluidBottom + FLUID_OVERLAP )
      .arc( BULB_CENTER_X, straightTubeTop, tubeFluidRadius, Math.PI, 0 ) // round top
      .lineTo( -tubeFluidLeft, tubeFluidBottom + FLUID_OVERLAP)
      .close();

    // Clip the top of the bulb so it's flat where it connects to the tube
    var bulbFluidClipArea = Shape.rectangle( tubeFluidBottom, BULB_CENTER_Y - options.bulbDiameter / 2, options.bulbDiameter, options.bulbDiameter );
    bulbFluidNode.setClipArea( bulbFluidClipArea );

    // Gradient for fluid in tube
    var tubeFluidGradient = new LinearGradient( tubeFluidLeft, 0, tubeFluidLeft + tubeFluidWidth, 0 )
      .addColorStop( 0, options.fluidMainColor )
      .addColorStop( 0.4, options.fluidHighlightColor )
      .addColorStop( 0.5, options.fluidHighlightColor )
      .addColorStop( 1, options.fluidMainColor );

    // Fluid in the tube (correct size set later)
    var tubeFluidNode = new Rectangle( 0, 0, tubeFluidWidth, 0, {
      fill: tubeFluidGradient,
      clipArea: fluidClipArea
    } );

    // Background inside the tube
    if ( options.backgroundFill ) {
      this.addChild( new Path( outlineShape, { fill: options.backgroundFill } ) );
    }

    // Add other nodes after optional background
    this.addChild( tubeFluidNode );
    this.addChild( bulbFluidNode );
    this.addChild( outlineNode );

    // Temperature determines the height of the fluid in the tube
    var maxFluidHeight = new Path( fluidClipArea ).height;
    //TODO this can exceed max/min. should this be clamped? or should it be replaced by dot.Util.linear?
    var temperatureLinearFunction = new LinearFunction( minTemperature, maxTemperature, 0, maxFluidHeight );

    var temperaturePropertyObserver = function( temp ) {
      var fluidHeight = temperatureLinearFunction( temp );
      tubeFluidNode.visible = ( fluidHeight > 0 );
      tubeFluidNode.setRect( tubeFluidLeft, tubeFluidBottom - fluidHeight, tubeFluidWidth, fluidHeight + FLUID_OVERLAP );
    };

    temperatureProperty.link( temperaturePropertyObserver );

    this.mutate( options );

    // @private
    this.disposeThermometerNode = function() {
      if ( temperatureProperty.hasListener( temperaturePropertyObserver ) ) {
        temperatureProperty.unlink( temperaturePropertyObserver );
      }
    };
  }

  sceneryPhet.register( 'ThermometerNode', ThermometerNode );

  return inherit( Node, ThermometerNode, {

    /**
     * Ensures that this node is subject to garbage collection
     * @public
     */
    dispose: function() {
      this.disposeThermometerNode();
      Node.prototype.dispose.call( this );
    }
  } );
} );<commit_msg>IntroScreenView.layout is no longer necessary, https://github.com/phetsims/projectile-motion/issues/164<commit_after>// Copyright 2014-2017, University of Colorado Boulder

/**
 * Thermometer node, see https://github.com/phetsims/scenery-phet/issues/43
 *
 * @author Aaron Davis
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var ShadedSphereNode = require( 'SCENERY_PHET/ShadedSphereNode' );
  var Shape = require( 'KITE/Shape' );

  // constants
  var FLUID_OVERLAP = 1; // overlap of fluid in tube and bulb, to hide seam
  // center of the bulb is at (0,0), let the client code move to the correct position
  var BULB_CENTER_X = 0;
  var BULB_CENTER_Y = 0;

  /**
   * @param {number} minTemperature
   * @param {number} maxTemperature
   * @param {Property.<number>} temperatureProperty
   * @param {Object} [options]
   * @constructor
   */
  function ThermometerNode( minTemperature, maxTemperature, temperatureProperty, options ) {
    options = _.extend( {
      bulbDiameter: 50,
      tubeWidth: 30,
      tubeHeight: 100,
      lineWidth: 4,
      outlineStroke: 'black',
      tickSpacing: 15,
      majorTickLength: 15,
      minorTickLength: 7.5,
      glassThickness: 2, // space between the thermometer outline and the fluid inside it

      // leave as null to have a transparent background. If a color is given, then an extra Rectangle is created for the background
      backgroundFill: null,

      // all the default colors are shades of red
      fluidMainColor: '#850e0e', // the main color of the bulb fluid, and the left side of the tube gradient
      fluidHighlightColor: '#ff7575', // the highlight color of the bulb fluid and the middle of the tube gradient
      fluidRightSideColor: '#c41515' // the right side of the tube gradient, not used currently
    }, options );

    Node.call( this );

    // Create a shaded sphere to act as the bulb fluid
    var bulbFluidDiameter = options.bulbDiameter - options.lineWidth - options.glassThickness; //TODO should this be options.lineWidth/2 ?
    var bulbFluidNode = new ShadedSphereNode( bulbFluidDiameter, {
      centerX: BULB_CENTER_X,
      centerY: BULB_CENTER_Y,
      mainColor: options.fluidMainColor,
      highlightColor: options.fluidHighlightColor,
      highlightXOffset: -0.2,
      highlightYOffset: 0.2,
      rotation: Math.PI / 2
    } );

    // Angles for the outline of the bulb
    var bulbStartAngle = -Math.acos( options.tubeWidth / options.bulbDiameter );
    var bulbEndAngle = Math.PI - bulbStartAngle;

    // Create the outline for the thermometer, starting with the bulb
    var tubeTopRadius = options.tubeWidth / 2;
    var straightTubeHeight = options.tubeHeight - tubeTopRadius;
    var straightTubeTop = BULB_CENTER_Y - ( options.bulbDiameter / 2 ) - straightTubeHeight;
    var straightTubeLeft = BULB_CENTER_X - ( options.tubeWidth / 2 );

    var outlineShape = new Shape()
      .arc( BULB_CENTER_X, BULB_CENTER_Y, options.bulbDiameter / 2, bulbStartAngle, bulbEndAngle ) // bulb at bottom
      .arc( BULB_CENTER_X, straightTubeTop, tubeTopRadius, Math.PI, 0 ) // rounded top of tube
      .close();
    // tick marks, from top down, alternating major and minor ticks
    var numberOfTicks = Math.floor( straightTubeHeight / options.tickSpacing ) + 1;
    for ( var i = 0; i < numberOfTicks; i++ ) {
      outlineShape.moveTo( straightTubeLeft, straightTubeTop + ( i * options.tickSpacing ) );
      outlineShape.horizontalLineTo( straightTubeLeft + ( ( i % 2 === 0 ) ? options.majorTickLength : options.minorTickLength ) );
    }

    var outlineNode = new Path( outlineShape, {
      stroke: options.outlineStroke,
      lineWidth: options.lineWidth
    } );
    assert && assert( outlineNode.height === options.tubeHeight + options.bulbDiameter + options.lineWidth ); // see scenery-phet#136

    var tubeFluidWidth = options.tubeWidth - options.lineWidth - options.glassThickness; //TODO should this be options.lineWidth/2 ?
    var tubeFluidRadius = tubeFluidWidth / 2;
    var clipBulbRadius = ( options.bulbDiameter - options.lineWidth - options.glassThickness ) / 2; //TODO should this be options.lineWidth/2 ?
    var clipStartAngle = -Math.acos( tubeFluidRadius / clipBulbRadius );
    var clipEndAngle = Math.PI - clipStartAngle;
    var tubeFluidBottom = ( bulbFluidDiameter / 2 ) * Math.sin( clipEndAngle );
    var tubeFluidLeft = -tubeFluidRadius;

    // Clip area for the fluid in the tube, round at the top
    var fluidClipArea = new Shape()
      .moveTo( tubeFluidLeft, tubeFluidBottom + FLUID_OVERLAP )
      .arc( BULB_CENTER_X, straightTubeTop, tubeFluidRadius, Math.PI, 0 ) // round top
      .lineTo( -tubeFluidLeft, tubeFluidBottom + FLUID_OVERLAP)
      .close();

    // Clip the top of the bulb so it's flat where it connects to the tube
    var bulbFluidClipArea = Shape.rectangle( tubeFluidBottom, BULB_CENTER_Y - options.bulbDiameter / 2, options.bulbDiameter, options.bulbDiameter );
    bulbFluidNode.setClipArea( bulbFluidClipArea );

    // Gradient for fluid in tube
    var tubeFluidGradient = new LinearGradient( tubeFluidLeft, 0, tubeFluidLeft + tubeFluidWidth, 0 )
      .addColorStop( 0, options.fluidMainColor )
      .addColorStop( 0.4, options.fluidHighlightColor )
      .addColorStop( 0.5, options.fluidHighlightColor )
      .addColorStop( 1, options.fluidMainColor );

    // Fluid in the tube (correct size set later)
    var tubeFluidNode = new Rectangle( 0, 0, tubeFluidWidth, 0, {
      fill: tubeFluidGradient,
      clipArea: fluidClipArea
    } );

    // Background inside the tube
    if ( options.backgroundFill ) {
      this.addChild( new Path( outlineShape, { fill: options.backgroundFill } ) );
    }

    // Add other nodes after optional background
    this.addChild( tubeFluidNode );
    this.addChild( bulbFluidNode );
    this.addChild( outlineNode );

    // Temperature determines the height of the fluid in the tube
    var maxFluidHeight = new Path( fluidClipArea ).height;
    //TODO this can exceed max/min. should this be clamped? or should it be replaced by dot.Util.linear?
    ivar temperatureLinearFunction = new LinearFunction( minTemperature, maxTemperature, 0, maxFluidHeight );

    var temperaturePropertyObserver = function( temp ) {
      var fluidHeight = temperatureLinearFunction( temp );
      tubeFluidNode.visible = ( fluidHeight > 0 );
      tubeFluidNode.setRect( tubeFluidLeft, tubeFluidBottom - fluidHeight, tubeFluidWidth, fluidHeight + FLUID_OVERLAP );
    };

    temperatureProperty.link( temperaturePropertyObserver );

    this.mutate( options );

    // @private
    this.disposeThermometerNode = function() {
      if ( temperatureProperty.hasListener( temperaturePropertyObserver ) ) {
        temperatureProperty.unlink( temperaturePropertyObserver );
      }
    };
  }

  sceneryPhet.register( 'ThermometerNode', ThermometerNode );

  return inherit( Node, ThermometerNode, {

    /**
     * Ensures that this node is subject to garbage collection
     * @public
     */
    dispose: function() {
      this.disposeThermometerNode();
      Node.prototype.dispose.call( this );
    }
  } );
} );<|endoftext|>