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

/**
 * A scenery node that looks like a key pad and allows the user to enter a number. The entered number is not
 * displayed by this node - it is intended to be used in conjunction with a separate display of some sort.
 *
 * @author John Blanco
 * @author Andrey Zelenkov (MLearner)
 * @author Chris Malley (PixelZoom, Inc.)
 *
 * @depracated - This keypad has been replaced by a more flexible and general version.
 * While there are no plans to go back and replace existing usages, new implementations should use Keypad.js.
 * See https://github.com/phetsims/scenery-phet/issues/283 for the history of this. -jbphet, Aug 2017
 */
define( function( require ) {
  'use strict';

  // modules
  var BackspaceIcon = require( 'SCENERY_PHET/BackspaceIcon' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var inherit = require( 'PHET_CORE/inherit' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var RectangularPushButton = require( 'SUN/buttons/RectangularPushButton' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var Tandem = require( 'TANDEM/Tandem' );
  var Text = require( 'SCENERY/nodes/Text' );
  var VBox = require( 'SCENERY/nodes/VBox' );

  // string
  var DECIMAL_POINT = '.'; //TODO localize, https://github.com/phetsims/scenery-phet/issues/279

  /**
   * @param {Object} [options]
   * @constructor
   */
  function NumberKeypad( options ) {
    Tandem.indicateUninstrumentedCode();

    options = _.extend( {
      buttonFont: new PhetFont( { size: 20 } ),
      minButtonWidth: 35,
      minButtonHeight: 35,
      decimalPointKey: false,
      xSpacing: 10,
      ySpacing: 10,
      keyColor: 'white',
      valueStringProperty: new Property( '' ),

      // {function(string, string)} validates a key press, see example and documentation in validateMaxDigits
      validateKey: validateMaxDigits( { maxDigits: 8 } )
    }, options );

    var self = this;

    // @public (read only) - sequence of key values entered by the user
    this.valueStringProperty = options.valueStringProperty;

    // @private - when true, the next key press will clear valueStringProperty
    this._clearOnNextKeyPress = false;

    // options for keys
    var keyOptions = {
      minWidth: options.minButtonWidth,
      minHeight: options.minButtonHeight,
      baseColor: options.keyColor,
      font: options.buttonFont
    };

    // create the backspace key
    var backspaceIcon = new BackspaceIcon();
    backspaceIcon.scale( Math.min( options.minButtonWidth / backspaceIcon.width * 0.7, ( options.minButtonHeight * 0.65 ) / backspaceIcon.height ) );
    var backspaceKey = new RectangularPushButton( {
      content: backspaceIcon,
      minWidth: keyOptions.minWidth,
      minHeight: keyOptions.minHeight,
      xMargin: 1,
      baseColor: keyOptions.baseColor,
      listener: function() {
        if ( self.valueStringProperty.value.length > 0 ) {

          // The backspace key ignores and resets the clearOnNextKeyPress flag. The rationale is that if a user has
          // entered an incorrect value and wants to correct it by using the backspace, then it should work like
          // the backspace always does instead of clearing the display.
          self._clearOnNextKeyPress = false;

          // Remove the last character
          self.valueStringProperty.set( self.valueStringProperty.get().slice( 0, -1 ) );
        }
      }
    } );

    /**
     * Called when a key is pressed.
     * @param {string} keyString - string associated with the key that was pressed
     */
    var keyCallback = function( keyString ) {

      // If set to clear the value on the next key press, clear the existing string.
      if ( self._clearOnNextKeyPress ) {
        self.valueStringProperty.value = '';
        self._clearOnNextKeyPress = false;
      }

      // process the keyString
      self.valueStringProperty.value = options.validateKey( keyString, self.valueStringProperty.value );
    };

    // create the bottom row of keys, which can vary based on options
    var bottomRowChildren = [];
    if ( options.decimalPointKey ) {

      // add a decimal point key plus a normal width zero key
      bottomRowChildren.push( createKey( DECIMAL_POINT, keyCallback, keyOptions ) );
      bottomRowChildren.push( createKey( '0', keyCallback, keyOptions ) );
    }
    else {

      // add a double-width zero key instead of the decimal point key
      var doubleRowButtonKeySpec = _.extend( {}, keyOptions, { minWidth: keyOptions.minWidth * 2 + options.xSpacing } );
      bottomRowChildren.push( createKey( '0', keyCallback, doubleRowButtonKeySpec ) );
    }
    bottomRowChildren.push( backspaceKey );

    // add the rest of the keys
    VBox.call( this, {
      spacing: options.ySpacing,
      children: [
        new HBox( {
          spacing: options.xSpacing,
          children: [
            createKey( '7', keyCallback, keyOptions ),
            createKey( '8', keyCallback, keyOptions ),
            createKey( '9', keyCallback, keyOptions )
          ]
        } ),
        new HBox( {
          spacing: options.xSpacing,
          children: [
            createKey( '4', keyCallback, keyOptions ),
            createKey( '5', keyCallback, keyOptions ),
            createKey( '6', keyCallback, keyOptions )
          ]
        } ),
        new HBox( {
          spacing: options.xSpacing,
          children: [
            createKey( '1', keyCallback, keyOptions ),
            createKey( '2', keyCallback, keyOptions ),
            createKey( '3', keyCallback, keyOptions )
          ]
        } ),
        new HBox( {
          spacing: options.xSpacing,
          children: bottomRowChildren
        } )
      ]
    } );

    this.mutate( options );
  }

  sceneryPhet.register( 'NumberKeypad', NumberKeypad );

  /**
   * Creates a key for the keypad.
   *
   * @param {string} keyString - string that appears on the key
   * @param {function(string)} callback - called when the key is pressed
   * @param {Object} keyOptions - see RectangularPushButton.options
   * @returns {RectangularPushButton}
   */
  function createKey( keyString, callback, keyOptions ) {
    return new RectangularPushButton( {
      content: new Text( keyString, { font: keyOptions.font } ),
      baseColor: keyOptions.baseColor,
      minWidth: keyOptions.minWidth,
      minHeight: keyOptions.minHeight,
      xMargin: 5,
      yMargin: 5,
      listener: function() { callback( keyString ); }
    } );
  }

  /**
   * Creates a validation function that constrains the value to a maximum number of digits, with 1 leading zero.
   *
   * @param {Object} [options]
   * @returns {function(string, string)}
   */
  var validateMaxDigits = function( options ) {

    options = _.extend( {
      maxDigits: 8 // {number} the maximum number of digits (numbers)
    }, options );
    assert && assert( options.maxDigits > 0, 'invalid maxDigits: ' + options.maxDigits );

    /**
     * Creates the new string that results from pressing a key.
     * @param {string} keyString - string associated with the key that was pressed
     * @param {string} valueString - string that corresponds to the sequence of keys that have been pressed
     * @returns {string} the result
     */
    return function( keyString, valueString ) {

      var hasDecimalPoint = valueString.indexOf( DECIMAL_POINT ) !== -1;
      var numberOfDigits = hasDecimalPoint ? valueString.length - 1 : valueString.length;

      var newValueString;
      if ( valueString === '0' && keyString === '0' ) {

        // ignore multiple leading zeros
        newValueString = valueString;
      }
      else if ( valueString === '0' && keyString !== '0' && keyString !== DECIMAL_POINT ) {

        // replace a leading 0 that's not followed by a decimal point with this key
        newValueString = keyString;
      }
      else if ( keyString !== DECIMAL_POINT && numberOfDigits < options.maxDigits ) {

        // constrain to maxDigits
        newValueString = valueString + keyString;
      }
      else if ( keyString === DECIMAL_POINT && valueString.indexOf( DECIMAL_POINT ) === -1 ) {

        // allow one decimal point
        newValueString = valueString + keyString;
      }
      else {

        // ignore keyString
        newValueString = valueString;
      }

      return newValueString;
    };
  };

  return inherit( VBox, NumberKeypad, {

    /**
     * Clear anything that has been accumulated in the valueStringProperty field.
     * @public
     */
    clear: function() {
      this.valueStringProperty.value = '';
    },

    /**
     * Determines whether pressing a key (except for the backspace) will clear the existing value.
     * @param {boolean} clearOnNextKeyPress
     * @public
     */
    setClearOnNextKeyPress: function( clearOnNextKeyPress ) {
      this._clearOnNextKeyPress = clearOnNextKeyPress;
    },
    set clearOnNextKeyPress( value ) { this.setClearOnNextKeyPress( value ); },

    /**
     * Will pressing a key (except for the backspace point) clear the existing value?
     * @returns {boolean}
     */
    getClearOnNextKeyPress: function() {
      return this._clearOnNextKeyPress;
    },
    get clearOnNextKeyPress() { return this.getClearOnNextKeyPress(); }
  }, {

    /**
     * Functions for creating useful values for options.validateKey
     * @public
     * @static
     */
    validateMaxDigits: validateMaxDigits,

    /**
     * String used for the decimal point. Needed for writing custom options.validateKey functions.
     * @public
     */
    DECIMAL_POINT: DECIMAL_POINT
  } );
} );
<commit_msg>Use read-only instead of read only<commit_after>// Copyright 2015-2017, University of Colorado Boulder

/**
 * A scenery node that looks like a key pad and allows the user to enter a number. The entered number is not
 * displayed by this node - it is intended to be used in conjunction with a separate display of some sort.
 *
 * @author John Blanco
 * @author Andrey Zelenkov (MLearner)
 * @author Chris Malley (PixelZoom, Inc.)
 *
 * @depracated - This keypad has been replaced by a more flexible and general version.
 * While there are no plans to go back and replace existing usages, new implementations should use Keypad.js.
 * See https://github.com/phetsims/scenery-phet/issues/283 for the history of this. -jbphet, Aug 2017
 */
define( function( require ) {
  'use strict';

  // modules
  var BackspaceIcon = require( 'SCENERY_PHET/BackspaceIcon' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var inherit = require( 'PHET_CORE/inherit' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var RectangularPushButton = require( 'SUN/buttons/RectangularPushButton' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var Tandem = require( 'TANDEM/Tandem' );
  var Text = require( 'SCENERY/nodes/Text' );
  var VBox = require( 'SCENERY/nodes/VBox' );

  // string
  var DECIMAL_POINT = '.'; //TODO localize, https://github.com/phetsims/scenery-phet/issues/279

  /**
   * @param {Object} [options]
   * @constructor
   */
  function NumberKeypad( options ) {
    Tandem.indicateUninstrumentedCode();

    options = _.extend( {
      buttonFont: new PhetFont( { size: 20 } ),
      minButtonWidth: 35,
      minButtonHeight: 35,
      decimalPointKey: false,
      xSpacing: 10,
      ySpacing: 10,
      keyColor: 'white',
      valueStringProperty: new Property( '' ),

      // {function(string, string)} validates a key press, see example and documentation in validateMaxDigits
      validateKey: validateMaxDigits( { maxDigits: 8 } )
    }, options );

    var self = this;

    // @public (read-only) - sequence of key values entered by the user
    this.valueStringProperty = options.valueStringProperty;

    // @private - when true, the next key press will clear valueStringProperty
    this._clearOnNextKeyPress = false;

    // options for keys
    var keyOptions = {
      minWidth: options.minButtonWidth,
      minHeight: options.minButtonHeight,
      baseColor: options.keyColor,
      font: options.buttonFont
    };

    // create the backspace key
    var backspaceIcon = new BackspaceIcon();
    backspaceIcon.scale( Math.min( options.minButtonWidth / backspaceIcon.width * 0.7, ( options.minButtonHeight * 0.65 ) / backspaceIcon.height ) );
    var backspaceKey = new RectangularPushButton( {
      content: backspaceIcon,
      minWidth: keyOptions.minWidth,
      minHeight: keyOptions.minHeight,
      xMargin: 1,
      baseColor: keyOptions.baseColor,
      listener: function() {
        if ( self.valueStringProperty.value.length > 0 ) {

          // The backspace key ignores and resets the clearOnNextKeyPress flag. The rationale is that if a user has
          // entered an incorrect value and wants to correct it by using the backspace, then it should work like
          // the backspace always does instead of clearing the display.
          self._clearOnNextKeyPress = false;

          // Remove the last character
          self.valueStringProperty.set( self.valueStringProperty.get().slice( 0, -1 ) );
        }
      }
    } );

    /**
     * Called when a key is pressed.
     * @param {string} keyString - string associated with the key that was pressed
     */
    var keyCallback = function( keyString ) {

      // If set to clear the value on the next key press, clear the existing string.
      if ( self._clearOnNextKeyPress ) {
        self.valueStringProperty.value = '';
        self._clearOnNextKeyPress = false;
      }

      // process the keyString
      self.valueStringProperty.value = options.validateKey( keyString, self.valueStringProperty.value );
    };

    // create the bottom row of keys, which can vary based on options
    var bottomRowChildren = [];
    if ( options.decimalPointKey ) {

      // add a decimal point key plus a normal width zero key
      bottomRowChildren.push( createKey( DECIMAL_POINT, keyCallback, keyOptions ) );
      bottomRowChildren.push( createKey( '0', keyCallback, keyOptions ) );
    }
    else {

      // add a double-width zero key instead of the decimal point key
      var doubleRowButtonKeySpec = _.extend( {}, keyOptions, { minWidth: keyOptions.minWidth * 2 + options.xSpacing } );
      bottomRowChildren.push( createKey( '0', keyCallback, doubleRowButtonKeySpec ) );
    }
    bottomRowChildren.push( backspaceKey );

    // add the rest of the keys
    VBox.call( this, {
      spacing: options.ySpacing,
      children: [
        new HBox( {
          spacing: options.xSpacing,
          children: [
            createKey( '7', keyCallback, keyOptions ),
            createKey( '8', keyCallback, keyOptions ),
            createKey( '9', keyCallback, keyOptions )
          ]
        } ),
        new HBox( {
          spacing: options.xSpacing,
          children: [
            createKey( '4', keyCallback, keyOptions ),
            createKey( '5', keyCallback, keyOptions ),
            createKey( '6', keyCallback, keyOptions )
          ]
        } ),
        new HBox( {
          spacing: options.xSpacing,
          children: [
            createKey( '1', keyCallback, keyOptions ),
            createKey( '2', keyCallback, keyOptions ),
            createKey( '3', keyCallback, keyOptions )
          ]
        } ),
        new HBox( {
          spacing: options.xSpacing,
          children: bottomRowChildren
        } )
      ]
    } );

    this.mutate( options );
  }

  sceneryPhet.register( 'NumberKeypad', NumberKeypad );

  /**
   * Creates a key for the keypad.
   *
   * @param {string} keyString - string that appears on the key
   * @param {function(string)} callback - called when the key is pressed
   * @param {Object} keyOptions - see RectangularPushButton.options
   * @returns {RectangularPushButton}
   */
  function createKey( keyString, callback, keyOptions ) {
    return new RectangularPushButton( {
      content: new Text( keyString, { font: keyOptions.font } ),
      baseColor: keyOptions.baseColor,
      minWidth: keyOptions.minWidth,
      minHeight: keyOptions.minHeight,
      xMargin: 5,
      yMargin: 5,
      listener: function() { callback( keyString ); }
    } );
  }

  /**
   * Creates a validation function that constrains the value to a maximum number of digits, with 1 leading zero.
   *
   * @param {Object} [options]
   * @returns {function(string, string)}
   */
  var validateMaxDigits = function( options ) {

    options = _.extend( {
      maxDigits: 8 // {number} the maximum number of digits (numbers)
    }, options );
    assert && assert( options.maxDigits > 0, 'invalid maxDigits: ' + options.maxDigits );

    /**
     * Creates the new string that results from pressing a key.
     * @param {string} keyString - string associated with the key that was pressed
     * @param {string} valueString - string that corresponds to the sequence of keys that have been pressed
     * @returns {string} the result
     */
    return function( keyString, valueString ) {

      var hasDecimalPoint = valueString.indexOf( DECIMAL_POINT ) !== -1;
      var numberOfDigits = hasDecimalPoint ? valueString.length - 1 : valueString.length;

      var newValueString;
      if ( valueString === '0' && keyString === '0' ) {

        // ignore multiple leading zeros
        newValueString = valueString;
      }
      else if ( valueString === '0' && keyString !== '0' && keyString !== DECIMAL_POINT ) {

        // replace a leading 0 that's not followed by a decimal point with this key
        newValueString = keyString;
      }
      else if ( keyString !== DECIMAL_POINT && numberOfDigits < options.maxDigits ) {

        // constrain to maxDigits
        newValueString = valueString + keyString;
      }
      else if ( keyString === DECIMAL_POINT && valueString.indexOf( DECIMAL_POINT ) === -1 ) {

        // allow one decimal point
        newValueString = valueString + keyString;
      }
      else {

        // ignore keyString
        newValueString = valueString;
      }

      return newValueString;
    };
  };

  return inherit( VBox, NumberKeypad, {

    /**
     * Clear anything that has been accumulated in the valueStringProperty field.
     * @public
     */
    clear: function() {
      this.valueStringProperty.value = '';
    },

    /**
     * Determines whether pressing a key (except for the backspace) will clear the existing value.
     * @param {boolean} clearOnNextKeyPress
     * @public
     */
    setClearOnNextKeyPress: function( clearOnNextKeyPress ) {
      this._clearOnNextKeyPress = clearOnNextKeyPress;
    },
    set clearOnNextKeyPress( value ) { this.setClearOnNextKeyPress( value ); },

    /**
     * Will pressing a key (except for the backspace point) clear the existing value?
     * @returns {boolean}
     */
    getClearOnNextKeyPress: function() {
      return this._clearOnNextKeyPress;
    },
    get clearOnNextKeyPress() { return this.getClearOnNextKeyPress(); }
  }, {

    /**
     * Functions for creating useful values for options.validateKey
     * @public
     * @static
     */
    validateMaxDigits: validateMaxDigits,

    /**
     * String used for the decimal point. Needed for writing custom options.validateKey functions.
     * @public
     */
    DECIMAL_POINT: DECIMAL_POINT
  } );
} );
<|endoftext|>


<commit_before>function UserControls(domElement){
	this.domElement = domElement;
	this.controlElements = [];
	this.rebuildFromMap = {};
	this.init();
}

UserControls.prototype.init = function() {
	var controlsAndDefaults = [
		{inputType: 'number', name: 'x', value: 33},
		{inputType: 'number', name: 'y', value: 3},
		//{inputType: 'slider', name: 'quality', value: 0.5},
		{inputType: 'checkbox', name: 'rivers', value: true},
		{inputType: 'slider', name: 'terrain', value: 0.25},
		{inputType: 'slider', name: 'scale', value: 0.5},
		{inputType: 'slider', name: 'sea level', value: 0.55},
		{inputType: 'slider', name: 'flat', value: 0.2},
		{inputType: 'slider', name: 'shore', value: 0.2},
		{inputType: 'slider', name: 'cityness', value: 0.11, rebuildFrom: 'roads'},
	];

	for (var i = 0; i < controlsAndDefaults.length; i++) {
		// Init control html
		var inputType = controlsAndDefaults[i].inputType;
		var name = controlsAndDefaults[i].name;
		var value = controlsAndDefaults[i].value;
		var controlElement = this[inputType](name, value);
		this.controlElements.push(controlElement);

		// init rebuildFromMap
		this.rebuildFromMap[controlElement.attr('id')] = controlsAndDefaults[i].rebuildFrom;
	};
};

UserControls.prototype.createDefualtInput = function(name, value) {
	$(this.domElement).append($(' ' + name + ':'));
	var $inputElement = $('<input>');
	$inputElement.attr('tabIndex','-1');
	var propertyName = name.replace(/ /g, '_');
	$inputElement.attr('id', propertyName);

	var self = this;
	$inputElement.change(function(event){
		var rebuildFrom = self.rebuildFromMap[event.target.id];
		rebuildScene(rebuildFrom);
	});

	$(this.domElement).append($inputElement);
	return $inputElement;
};

UserControls.prototype.slider = function(name, value) {
	$inputElement = this.createDefualtInput(name, value);
	$inputElement.attr('value', 100*value);
	$inputElement.attr('type', 'range');
	return $inputElement;
};

UserControls.prototype.number = function(name, value) {
	$inputElement = this.createDefualtInput(name, value);
	$inputElement.attr('value', value);
	$inputElement.attr('type', 'number');
	return $inputElement;
};

UserControls.prototype.checkbox = function(name, value) {
	$inputElement = this.createDefualtInput(name, value);
	$inputElement.attr('checked', value);
	$inputElement.attr('type', 'checkbox');
	return $inputElement;
};

UserControls.prototype.getControls = function() {
	var controls = {};
	for (var i = 0; i < this.controlElements.length; i++) {
		var control = this.controlElements[i];
		if(control.attr('type') === 'range'){
			controls[control.attr('id')] = 0.01*Number(control[0].value);
		}
		if(control.attr('type') === 'number'){
			controls[control.attr('id')] = Number(control[0].value);
		}
		if(control.attr('type') === 'checkbox'){
			controls[control.attr('id')] = control[0].checked;
		}
	};

	// Add default quality 0.5
	controls.quality = 0.5;
	return controls;
};<commit_msg>simplified user interface<commit_after>function UserControls(domElement){
	this.domElement = domElement;
	this.controlElements = [];
	this.rebuildFromMap = {};
	this.init();
}

UserControls.prototype.init = function() {
	var controlsAndDefaults = [
		{inputType: 'number', name: 'x', value: 33},
		{inputType: 'number', name: 'y', value: 3},
		{inputType: 'slider', name: 'quality', value: 0.5, display: 'none'},
		{inputType: 'checkbox', name: 'rivers', value: true},
		{inputType: 'slider', name: 'terrain', value: 0.69},
		{inputType: 'slider', name: 'scale', value: 0.72},
		{inputType: 'slider', name: 'sea level', value: 0.55},
		{inputType: 'slider', name: 'flat', value: 0.2},
		{inputType: 'slider', name: 'shore', value: 0.13, display: 'none'},
		{inputType: 'slider', name: 'cityness', value: 0.24, rebuildFrom: 'roads'},
	];

	for (var i = 0; i < controlsAndDefaults.length; i++) {
		// Init control html
		var inputType = controlsAndDefaults[i].inputType;
		var name = controlsAndDefaults[i].name;
		var value = controlsAndDefaults[i].value;
		var display = controlsAndDefaults[i].display === undefined ? 'inline-block' : controlsAndDefaults[i].display;
		var controlElement = this[inputType](name, value, display);
		this.controlElements.push(controlElement);

		// init rebuildFromMap
		this.rebuildFromMap[controlElement.attr('id')] = controlsAndDefaults[i].rebuildFrom;
	};
};

UserControls.prototype.createDefualtInput = function(name, value, display) {
	// Label element
	var $labelElement = $(' ' + name + ':');
	$labelElement.attr('style', 'display:' + display);

	// input element
	var $inputElement = $('<input>');
	$inputElement.attr('tabIndex','-1');
	var propertyName = name.replace(/ /g, '_');
	$inputElement.attr('id', propertyName);
	$inputElement.attr('style', 'display:' + display);

	var self = this;
	$inputElement.change(function(event){
		var rebuildFrom = self.rebuildFromMap[event.target.id];
		var controls = self.getControls();
		//rebuildScene(rebuildFrom);
		worldFactory.createAndSetScene(controls, rebuildFrom);
	});

	$(this.domElement).append($labelElement);
	$(this.domElement).append($inputElement);

	return $inputElement;
};

UserControls.prototype.slider = function(name, value, display) {
	$inputElement = this.createDefualtInput(name, value, display);
	$inputElement.attr('value', 100*value);
	$inputElement.attr('type', 'range');
	return $inputElement;
};

UserControls.prototype.number = function(name, value, display) {
	$inputElement = this.createDefualtInput(name, value, display);
	$inputElement.attr('value', value);
	$inputElement.attr('type', 'number');
	return $inputElement;
};

UserControls.prototype.checkbox = function(name, value, display) {
	$inputElement = this.createDefualtInput(name, value, display);
	$inputElement.attr('checked', value);
	$inputElement.attr('type', 'checkbox');
	return $inputElement;
};

UserControls.prototype.getControls = function() {
	var controls = {};
	for (var i = 0; i < this.controlElements.length; i++) {
		var control = this.controlElements[i];
		if(control.attr('type') === 'range'){
			controls[control.attr('id')] = 0.01*Number(control[0].value);
		}
		if(control.attr('type') === 'number'){
			controls[control.attr('id')] = Number(control[0].value);
		}
		if(control.attr('type') === 'checkbox'){
			controls[control.attr('id')] = control[0].checked;
		}
	};
	return controls;
};<|endoftext|>


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

/**
 * An XYPlot that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 */

import Utils from '../../dot/js/Utils.js';
import merge from '../../phet-core/js/merge.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Circle from '../../scenery/js/nodes/Circle.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Color from '../../scenery/js/util/Color.js';
import Tandem from '../../tandem/js/Tandem.js';
import griddle from './griddle.js';
import XYPlot from './XYPlot.js';

// constants
const WIDTH_PROPORTION = 0.013; // empirically determined
const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
const CURSOR_STROKE_COLOR = Color.DARK_GRAY;

class XYCursorPlot extends XYPlot {

  constructor( options ) {

    options = merge( {

      // options passed on to the chart cursor, see ChartCursor
      cursorOptions: null,

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    super( options );

    // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
    this._cursorVisibleOverride = null;

    // @private {number} - value for the cursor, determines cursor positioning
    this.cursorValue = 0;

    // @private - minimum and maximum recorded value, required by the cursor to limit dragging
    this.minRecordedValue = 0;
    this.maxRecordedValue = 0;

    // @private - Keep track of listeners for each series so the listeners can be removed when the series is removed
    this.valueSeriesListenerMap = new Map();

    // @private
    this.chartCursor = new ChartCursor( this, options.cursorOptions );
    this.addChild( this.chartCursor );

    // initialize position and visibility of the cursor
    this.updateChartCursor();
  }

  /**
   * Add a series to the plot, first adding a listener that keeps track of minimum and maximum data values.
   * @public
   * @override
   *
   * @param {DynamicSeries} series
   * @param {number} scaleFactor
   */
  addSeries( series, scaleFactor ) {
    super.addSeries( series, scaleFactor );

    // when a point is added, update the min and max recorded values
    const seriesListener = () => {

      // update min/max domain of the plotted data
      this.updateMinMaxXValues();

      // if all data has been removed from the plot, update cursor visibility
      this.updateChartCursor();
    };

    // save to map so that listener can be found again for disposal
    this.valueSeriesListenerMap.set( series, seriesListener );
    series.addDynamicSeriesListener( seriesListener );
  }

  /**
   * Remove a series from the plot and dispose of the plot specific series listener.
   * @public
   *
   * @param {DynamicSeries} series - to remove
   */
  removeSeries( series ) {
    series.removeDynamicSeriesListener( this.valueSeriesListenerMap.get( series ) );
    this.valueSeriesListenerMap.delete( series );
    super.removeSeries( series );
  }

  /**
   * Set the cursor value.
   * @public
   *
   * @param {number} value
   */
  setCursorValue( value ) {
    this.cursorValue = value;
    this.updateChartCursor();
  }

  /**
   * Get the value currently under the cursor.
   * @public
   *
   * @returns {number}
   */
  getCursorValue() {
    return this.cursorValue;
  }

  /**
   * Override the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
   * described in updateChartCursorVisibility. Otherwise, visibility will equal the boolean value set here.
   * @param {boolean|null} visible
   */
  setCursorVisibleOverride( visible ) {
    assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
    this._cursorVisibleOverride = visible;
    this.updateChartCursorVisibility();
  }

  /**
   * Update the chart cursor visibility and position.
   * @private
   */
  updateChartCursor() {
    this.updateChartCursorVisibility();
    if ( this.chartCursor.isVisible() ) {
      this.updateChartCursorPos();
    }
  }

  /**
   * Update the chart cursor position.
   * @private
   */
  updateChartCursorPos() {
    const recordingStartValue = this.getMinRecordedValue();
    const recordingCurrentValue = this.cursorValue;
    this.moveChartCursorToValue( ( recordingCurrentValue - recordingStartValue ) );
  }

  /**
   * Update the chart cursor visibility. The chart cursor should be visible any time the cursor value is within
   * the recorded value range.
   *
   * @private
   */
  updateChartCursorVisibility() {
    if ( typeof this._cursorVisibleOverride === 'boolean' ) {
      this.chartCursor.setVisible( this._cursorVisibleOverride );
    }
    else {
      const valueOnChart = ( this.cursorValue - this.getMinRecordedValue() );
      const isCurrentValueOnChart = ( valueOnChart >= 0 ) && ( valueOnChart <= this.maxX );
      const dataExists = this.getDataExists();
      const chartCursorVisible = isCurrentValueOnChart && dataExists;
      this.chartCursor.setVisible( chartCursorVisible );
    }
  }

  /**
   * Get the chart value for the given position in view coordinates, relative tot the parent coordinate frame.
   * @private
   *
   * @param {number} position
   * @returns {number}
   */
  positionToValue( position ) {
    return position * ( this.maxX - this.minX ) / this.plotPath.width;
  }

  /**
   * Get the position on the chart from the provided value.
   * @private
   *
   * @param {number} value
   * @returns {number}
   */
  valueToPosition( value ) {
    return value * this.plotPath.width / ( this.maxX - this.minX );
  }

  /**
   * Move the chart cursor to the specified value.
   * @private
   *
   * @param {number} value
   */
  moveChartCursorToValue( value ) {

    // origin of cursor is at the center
    const xPosition = value * this.plotPath.width / ( this.maxX - this.minX );
    this.chartCursor.centerX = Utils.clamp( xPosition, 0, this.plotPath.width );
    this.chartCursor.centerY = this.plotPath.centerY;
  }

  /**
   * Get the minimum X data value in the data series lists. Returns zero if no data has been added yet. This value
   * is updated whenever data is added to the list.
   *
   * @returns {number}
   */
  getMinRecordedValue() {
    return !this.getDataExists() ? 0 : this.minRecordedValue;
  }

  /**
   * Get the maximum X data value in the data series lists. Returns zero if no data has been added yet.  This value
   * is updated whenever data is added to the data series list.
   *
   * @returns {number}
   */
  getMaxRecordedValue() {
    return !this.getDataExists() ? 0 : this.maxRecordedValue;
  }

  /**
   * Update the minimum/maximum plotted domain of the data recorded on this plot. This information is used
   * to determine selected cursor values. If no data is associated with this plot, extrema are represented by
   * min = infinity, max = negative infinity.
   * @private
   */
  updateMinMaxXValues() {
    this.minRecordedValue = Number.POSITIVE_INFINITY;
    this.maxRecordedValue = Number.NEGATIVE_INFINITY;
    for ( let i = 0; i < this.dataSeriesList.length; i++ ) {
      const dataSeries = this.dataSeriesList[ i ];
      for ( let j = 0; j < dataSeries.getLength(); j++ ) {
        const xValue = dataSeries.getDataPoint( j ).x;

        if ( xValue > this.maxRecordedValue ) {
          this.maxRecordedValue = xValue;
        }
        if ( xValue < this.minRecordedValue ) {
          this.minRecordedValue = xValue;
        }
      }
    }
  }
}

/**
 * Rectangular cursor that indicates a current or selected value on the chart.
 */
class ChartCursor extends Rectangle {

  /**
   * @param {XYPlot} plot
   * @param {number} options
   */
  constructor( plot, options ) {

    options = merge( {
      startDrag: () => {},
      endDrag: () => {},
      drag: () => {},

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    const width = plot.plotPath.width * WIDTH_PROPORTION;
    const height = plot.plotPath.height;

    // Set the shape. Origin is at the center top of the rectangle.
    super( 0, -height, width, height, 0, 0, {
      cursor: 'e-resize',
      fill: CURSOR_FILL_COLOR,
      stroke: CURSOR_STROKE_COLOR,
      lineWidth: 0.4,
      lineDash: [ 4, 4 ]
    } );

    this.plot = plot;

    // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
    this.mouseArea = this.localBounds.dilatedX( 12 );
    this.touchArea = this.localBounds.dilatedX( 12 );

    // Add the indentations that are intended to convey the idea of "gripability".
    const grippyNode = new Node();
    const indentSpacing = height * 0.05;
    for ( let i = 0; i < 3; i++ ) {
      const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
      indentNode.top = i * ( indentNode.height + indentSpacing );
      grippyNode.addChild( indentNode );
    }
    grippyNode.center = this.center;
    this.addChild( grippyNode );

    const dragListener = new DragListener( {
      start: ( event, listener ) => {
        assert && assert( this.plot.getDataExists(), 'data should exist for the cursor to be draggable' );
        options.startDrag();
      },
      drag: ( event, listener ) => {
        const parentX = listener.parentPoint.x;
        let newValue = this.plot.positionToValue( parentX );

        // limit cursor to the domain of recorded values
        newValue = Utils.clamp( newValue, plot.getMinRecordedValue(), plot.getMaxRecordedValue() );
        this.plot.setCursorValue( newValue );

        options.drag();
      },
      end: ( event, listener ) => {
        options.endDrag();
      },
      tandem: options.tandem.createTandem( 'dragListener' )
    } );
    this.addInputListener( dragListener );
  }
}

/**
 * This node is meant to portray a small round indentation on a surface.  This is a modern user interface paradigm that
 * is intended to convey the concept of "gripability" (sp?), i.e. something that the user can click on and subsequently
 * grab.  This is meant to look somewhat 3D, much like etched borders do.
 */
class GrippyIndentNode extends Circle {

  /**
   * @param {number} diameter
   * @param {Color} baseColor
   * @param {Object} [options]
   */
  constructor( diameter, baseColor, options ) {

    options = merge( {
      lineWidth: 0.5
    }, options );

    const baseDarkerColor = baseColor.darkerColor( 0.9 );
    const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
      baseDarkerColor.getBlue(), baseColor.getAlpha() );
    const baseLighterColor = baseColor.brighterColor( 0.9 );
    const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
      baseLighterColor.getBlue(), baseColor.getAlpha() );
    const radius = diameter / 2 - options.lineWidth;

    super( radius, {
      fill: translucentDarkerColor,
      stroke: translucentBrighterColor,
      lineWidth: options.lineWidth
    } );
  }
}

griddle.register( 'XYCursorPlot', XYCursorPlot );
export default XYCursorPlot;<commit_msg>Formatting and REVIEW comments, see https://github.com/phetsims/energy-skate-park/issues/202<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * An XYPlot that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 */

import Utils from '../../dot/js/Utils.js';
import merge from '../../phet-core/js/merge.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Circle from '../../scenery/js/nodes/Circle.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Color from '../../scenery/js/util/Color.js';
import Tandem from '../../tandem/js/Tandem.js';
import griddle from './griddle.js';
import XYPlot from './XYPlot.js';

// constants
const WIDTH_PROPORTION = 0.013; // empirically determined
const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
const CURSOR_STROKE_COLOR = Color.DARK_GRAY;

class XYCursorPlot extends XYPlot {

  constructor( options ) {

    options = merge( {

      // options passed on to the chart cursor, see ChartCursor
      cursorOptions: null,

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    super( options );

    // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
    this._cursorVisibleOverride = null;

    // @private {number} - value for the cursor, determines cursor positioning
    this.cursorValue = 0;

    // @private - minimum and maximum recorded value, required by the cursor to limit dragging
    this.minRecordedValue = 0;
    this.maxRecordedValue = 0;

    // @private - Keep track of listeners for each series so the listeners can be removed when the series is removed
    this.valueSeriesListenerMap = new Map();

    // @private
    this.chartCursor = new ChartCursor( this, options.cursorOptions );
    this.addChild( this.chartCursor );

    // initialize position and visibility of the cursor
    this.updateChartCursor();
  }

  /**
   * Add a series to the plot, first adding a listener that keeps track of minimum and maximum data values.
   * @public
   * @override
   *
   * @param {DynamicSeries} series
   * @param {number} scaleFactor
   */
  addSeries( series, scaleFactor ) {
    super.addSeries( series, scaleFactor );

    // when a point is added, update the min and max recorded values
    const seriesListener = () => {

      // update min/max domain of the plotted data
      this.updateMinMaxXValues();

      // if all data has been removed from the plot, update cursor visibility
      this.updateChartCursor();
    };

    // save to map so that listener can be found again for disposal
    this.valueSeriesListenerMap.set( series, seriesListener );
    series.addDynamicSeriesListener( seriesListener );
  }

  /**
   * Remove a series from the plot and dispose of the plot specific series listener.
   * @public
   *
   * @param {DynamicSeries} series - to remove
   */
  removeSeries( series ) {
    series.removeDynamicSeriesListener( this.valueSeriesListenerMap.get( series ) );
    this.valueSeriesListenerMap.delete( series );
    super.removeSeries( series );
  }

  /**
   * Set the cursor value.
   * @public
   *
   * @param {number} value
   */
  setCursorValue( value ) {
    this.cursorValue = value;
    this.updateChartCursor();
  }

  /**
   * Get the value currently under the cursor.
   * @public
   *
   * @returns {number}
   */
  getCursorValue() {
    return this.cursorValue;
  }

  /**
   * Override the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
   * described in updateChartCursorVisibility. Otherwise, visibility will equal the boolean value set here.
   * @param {boolean|null} visible
   */
  setCursorVisibleOverride( visible ) {
    assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
    this._cursorVisibleOverride = visible;
    this.updateChartCursorVisibility();
  }

  /**
   * Update the chart cursor visibility and position.
   * @private
   */
  updateChartCursor() {
    this.updateChartCursorVisibility();
    if ( this.chartCursor.isVisible() ) {
      this.updateChartCursorPos();
    }
  }

  /**
   * Update the chart cursor position.
   * @private
   */
  updateChartCursorPos() {
    const recordingStartValue = this.getMinRecordedValue();
    const recordingCurrentValue = this.cursorValue;
    this.moveChartCursorToValue( recordingCurrentValue - recordingStartValue );
  }

  /**
   * Update the chart cursor visibility. The chart cursor should be visible any time the cursor value is within
   * the recorded value range.
   *
   * @private
   */
  updateChartCursorVisibility() {
    if ( typeof this._cursorVisibleOverride === 'boolean' ) {
      this.chartCursor.setVisible( this._cursorVisibleOverride );
    }
    else {
      const valueOnChart = ( this.cursorValue - this.getMinRecordedValue() );
      const isCurrentValueOnChart = ( valueOnChart >= 0 ) && ( valueOnChart <= this.maxX );
      const dataExists = this.getDataExists();
      const chartCursorVisible = isCurrentValueOnChart && dataExists;
      this.chartCursor.setVisible( chartCursorVisible );
    }
  }

  /**
   * Get the chart value for the given position in view coordinates, relative tot the parent coordinate frame.
   * @private
   *
   * @param {number} position
   * @returns {number}
   */
  positionToValue( position ) {
    return position * ( this.maxX - this.minX ) / this.plotPath.width;
  }

  /**
   * Get the position on the chart from the provided value.
   * @private
   *
   * @param {number} value
   * @returns {number}
   */
  valueToPosition( value ) {
    return value * this.plotPath.width / ( this.maxX - this.minX );
  }

  /**
   * Move the chart cursor to the specified value.
   * @private
   *
   * @param {number} value
   */
  moveChartCursorToValue( value ) {

    // origin of cursor is at the center
    const xPosition = value * this.plotPath.width / ( this.maxX - this.minX );
    this.chartCursor.centerX = Utils.clamp( xPosition, 0, this.plotPath.width );
    this.chartCursor.centerY = this.plotPath.centerY;
  }

  /**
   * Get the minimum X data value in the data series lists. Returns zero if no data has been added yet. This value
   * is updated whenever data is added to the list.
   *
   * @returns {number}
   */
  getMinRecordedValue() {
    return !this.getDataExists() ? 0 : this.minRecordedValue;
  }

  /**
   * Get the maximum X data value in the data series lists. Returns zero if no data has been added yet.  This value
   * is updated whenever data is added to the data series list.
   *
   * @returns {number}
   */
  getMaxRecordedValue() {
    return !this.getDataExists() ? 0 : this.maxRecordedValue;
  }

  /**
   * Update the minimum/maximum plotted domain of the data recorded on this plot. This information is used
   * to determine selected cursor values. If no data is associated with this plot, extrema are represented by
   * min = infinity, max = negative infinity.
   * @private
   */
  updateMinMaxXValues() {
    this.minRecordedValue = Number.POSITIVE_INFINITY;
    this.maxRecordedValue = Number.NEGATIVE_INFINITY;
    for ( let i = 0; i < this.dataSeriesList.length; i++ ) {
      const dataSeries = this.dataSeriesList[ i ];
      for ( let j = 0; j < dataSeries.getLength(); j++ ) {
        const xValue = dataSeries.getDataPoint( j ).x;

        if ( xValue > this.maxRecordedValue ) {
          this.maxRecordedValue = xValue;
        }
        if ( xValue < this.minRecordedValue ) {
          this.minRecordedValue = xValue;
        }
      }
    }
  }
}

/**
 * Rectangular cursor that indicates a current or selected value on the chart.
 */
class ChartCursor extends Rectangle {

  /**
   * @param {XYPlot} plot
   * @param {number} options
   */
  constructor( plot, options ) {

    options = merge( {
      startDrag: () => {},
      endDrag: () => {},
      drag: () => {},

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    const width = plot.plotPath.width * WIDTH_PROPORTION;
    const height = plot.plotPath.height;

    // Set the shape. Origin is at the center top of the rectangle.
    super( 0, -height, width, height, 0, 0, {
      cursor: 'e-resize',
      fill: CURSOR_FILL_COLOR,
      stroke: CURSOR_STROKE_COLOR,
      lineWidth: 0.4,
      lineDash: [ 4, 4 ]
    } );

    this.plot = plot;

    // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
    this.mouseArea = this.localBounds.dilatedX( 12 );
    this.touchArea = this.localBounds.dilatedX( 12 );

    // Add the indentations that are intended to convey the idea of "gripability".
    const grippyNode = new Node();
    const indentSpacing = height * 0.05;
    for ( let i = 0; i < 3; i++ ) {
      const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
      indentNode.top = i * ( indentNode.height + indentSpacing );
      grippyNode.addChild( indentNode );
    }
    grippyNode.center = this.center;
    this.addChild( grippyNode );

    const dragListener = new DragListener( {
      start: ( event, listener ) => {
        assert && assert( this.plot.getDataExists(), 'data should exist for the cursor to be draggable' );
        options.startDrag();
      },
      drag: ( event, listener ) => {
        const parentX = listener.parentPoint.x;
        let newValue = this.plot.positionToValue( parentX );

        // limit cursor to the domain of recorded values
        newValue = Utils.clamp( newValue, plot.getMinRecordedValue(), plot.getMaxRecordedValue() );
        this.plot.setCursorValue( newValue );

        options.drag();
      },
      end: ( event, listener ) => {
        options.endDrag();
      },
      tandem: options.tandem.createTandem( 'dragListener' )
    } );
    this.addInputListener( dragListener );
  }
}

/**
 * This node is meant to portray a small round indentation on a surface.  This is a modern user interface paradigm that
 * is intended to convey the concept of "gripability" (sp?), i.e. something that the user can click on and subsequently
 * grab.  This is meant to look somewhat 3D, much like etched borders do.
 */
class GrippyIndentNode extends Circle {

  /**
   * @param {number} diameter
   * @param {Color} baseColor
   * @param {Object} [options]
   */
  constructor( diameter, baseColor, options ) {

    options = merge( {
      lineWidth: 0.5
    }, options );

    const baseDarkerColor = baseColor.darkerColor( 0.9 );
    const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
      baseDarkerColor.getBlue(), baseColor.getAlpha() );
    const baseLighterColor = baseColor.brighterColor( 0.9 );
    const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
      baseLighterColor.getBlue(), baseColor.getAlpha() );
    const radius = diameter / 2 - options.lineWidth;

    super( radius, {
      fill: translucentDarkerColor,
      stroke: translucentBrighterColor,
      lineWidth: options.lineWidth
    } );
  }
}

griddle.register( 'XYCursorPlot', XYCursorPlot );
export default XYCursorPlot;<|endoftext|>


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

/**
 * An XYPlot that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 */
define( require => {
  'use strict';

  // modules
  const Circle = require( 'SCENERY/nodes/Circle' );
  const Color = require( 'SCENERY/util/Color' );
  const DragListener = require( 'SCENERY/listeners/DragListener' );
  const griddle = require( 'GRIDDLE/griddle' );
  const merge = require( 'PHET_CORE/merge' );
  const Node = require( 'SCENERY/nodes/Node' );
  const Rectangle = require( 'SCENERY/nodes/Rectangle' );
  const Tandem = require( 'TANDEM/Tandem' );
  const Utils = require( 'DOT/Utils' );
  const XYPlot = require( 'GRIDDLE/XYPlot' );

  // constants
  const WIDTH_PROPORTION = 0.013; // empirically determined
  const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
  const CURSOR_STROKE_COLOR = Color.DARK_GRAY;

  class XYCursorPlot extends XYPlot {

    constructor( options ) {

      options = merge( {

        // options passed on to the chart cursor, see ChartCursor
        cursorOptions: null,

        // phet-io
        tandem: Tandem.OPTIONAL
      }, options );

      super( options );

      // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
      this._cursorVisibleOverride = null;

      // @private {number} - value for the cursor, determines cursor positioning
      this.cursorValue = 0;

      // @private - minimum and maximum recorded value, required by the cursor to limit dragging
      this.minRecordedValue = 0;
      this.maxRecordedValue = 0;

      // @private - Keep track of listeners for each series so the listeners can be removed when the series is removed
      this.valueSeriesListenerMap = new Map();

      // @private
      this.chartCursor = new ChartCursor( this, options.cursorOptions );
      this.addChild( this.chartCursor );

      // initialize position and visibility of the cursor
      this.updateChartCursor();
    }

    /**
     * Add a series to the plot, first adding a listener that keeps track of minimum and maximum data values.
     * @public
     * @override
     *
     * @param {DynamicSeries} series
     * @param {number} scaleFactor
     */
    addSeries( series, scaleFactor ) {
      super.addSeries( series, scaleFactor );

      // when a point is added, update the min and max recorded values
      const seriesListener = () => {

        // update min/max domain of the plotted data
        this.updateMinMaxXValues();

        // if all data has been removed from the plot, update cursor visibility
        this.updateChartCursor();
      };

      // save to map so that listener can be found again for disposal
      this.valueSeriesListenerMap.set( series, seriesListener );
      series.addDynamicSeriesListener( seriesListener );
    }

    /**
     * Remove a series from the plot and dispose of the plot specific series listener.
     * @public
     *
     * @param {DynamicSeries} series - to remove
     */
    removeSeries( series ) {
      series.removeDynamicSeriesListener( this.valueSeriesListenerMap.get( series ) );
      this.valueSeriesListenerMap.delete( series );
      super.removeSeries( series );
    }

    /**
     * Set the cursor value.
     * @public
     *
     * @param {number} value
     */
    setCursorValue( value ) {
      this.cursorValue = value;
      this.updateChartCursor();
    }

    /**
     * Get the value currently under the cursor.
     * @public
     *
     * @returns {number}
     */
    getCursorValue() {
      return this.cursorValue;
    }

    /**
     * Override the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
     * described in updateChartCursorVisibility. Otherwise, visibility will equal the boolean value set here.
     * @param {boolean|null} visible
     */
    setCursorVisibleOverride( visible ) {
      assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
      this._cursorVisibleOverride = visible;
      this.updateChartCursorVisibility();
    }

    /**
     * Update the chart cursor visibility and position.
     * @private
     */
    updateChartCursor() {
      this.updateChartCursorVisibility();
      if ( this.chartCursor.isVisible() ) {
        this.updateChartCursorPos();
      }
    }

    /**
     * Update the chart cursor position.
     * @private
     */
    updateChartCursorPos() {
      const recordingStartValue = this.getMinRecordedValue();
      const recordingCurrentValue = this.cursorValue;
      this.moveChartCursorToValue( ( recordingCurrentValue - recordingStartValue ) );
    }

    /**
     * Update the chart cursor visibility. The chart cursor should be visible any time the cursor value is within
     * the recorded value range.
     *
     * @private
     */
    updateChartCursorVisibility() {
      if ( typeof this._cursorVisibleOverride === 'boolean' ) {
        this.chartCursor.setVisible( this._cursorVisibleOverride );
      }
      else {
        const valueOnChart = ( this.cursorValue - this.getMinRecordedValue() );
        const isCurrentValueOnChart = ( valueOnChart >= 0 ) && ( valueOnChart <= this.maxX );
        const dataExists = this.getDataExists();
        const chartCursorVisible = isCurrentValueOnChart && dataExists;
        this.chartCursor.setVisible( chartCursorVisible );
      }
    }

    /**
     * Get the chart value for the given position in view coordinates, relative tot the parent coordinate frame.
     * @private
     *
     * @param {number} position
     * @returns {number}
     */
    positionToValue( position ) {
      return position * ( this.maxX - this.minX ) / this.plotPath.width;
    }

    /**
     * Get the position on the chart from the provided value.
     * @private
     *
     * @param {number} value
     * @returns {number}
     */
    valueToPosition( value ) {
      return value * this.plotPath.width / ( this.maxX - this.minX );
    }

    /**
     * Move the chart cursor to the specified value.
     * @private
     *
     * @param {number} value
     */
    moveChartCursorToValue( value ) {

      // origin of cursor is at the center
      const xPosition = value * this.plotPath.width / ( this.maxX - this.minX );
      this.chartCursor.centerX = Utils.clamp( xPosition, 0, this.plotPath.width );
      this.chartCursor.centerY = this.plotPath.centerY;
    }

    /**
     * Get the minimum X data value in the data series lists. Returns zero if no data has been added yet. This value
     * is updated whenever data is added to the list.
     *
     * @returns {number}
     */
    getMinRecordedValue() {
      return !this.getDataExists() ? 0 : this.minRecordedValue;
    }

    /**
     * Get the maximum X data value in the data series lists. Returns zero if no data has been added yet.  This value
     * is updated whenever data is added to the data series list.
     *
     * @returns {number}
     */
    getMaxRecordedValue() {
      return !this.getDataExists() ? 0 : this.maxRecordedValue;
    }

    /**
     * Update the minimum/maximum plotted domain of the data recorded on this plot. This information is used
     * to determine selected cursor values. If no data is associated with this plot, extrema are represented by
     * min = infinity, max = negative infinity.
     * @private
     */
    updateMinMaxXValues() {
      this.minRecordedValue = Number.POSITIVE_INFINITY;
      this.maxRecordedValue = Number.NEGATIVE_INFINITY;
      for ( let i = 0; i < this.dataSeriesList.length; i++ ) {
        const dataSeries = this.dataSeriesList[ i ];
        for ( let j = 0; j < dataSeries.getLength(); j++ ) {
          const xValue = dataSeries.getDataPoint( j ).x;

          if ( xValue > this.maxRecordedValue ) {
            this.maxRecordedValue = xValue;
          }
          if ( xValue < this.minRecordedValue ) {
            this.minRecordedValue = xValue;
          }
        }
      }
    }
  }

  /**
   * Rectangular cursor that indicates a current or selected value on the chart.
   */
  class ChartCursor extends Rectangle {

    /**
     * @param {XYPlot} plot
     * @param {number} options
     */
    constructor( plot, options ) {

      options = merge( {
        startDrag: () => {},
        endDrag: () => {},
        drag: () => {},

        // phet-io
        tandem: Tandem.OPTIONAL
      }, options );

      const width = plot.plotPath.width * WIDTH_PROPORTION;
      const height = plot.plotPath.height;

      // Set the shape. Origin is at the center top of the rectangle.
      super( 0, -height, width, height, 0, 0, {
        cursor: 'e-resize',
        fill: CURSOR_FILL_COLOR,
        stroke: CURSOR_STROKE_COLOR,
        lineWidth: 0.4,
        lineDash: [ 4, 4 ]
      } );

      this.plot = plot;

      // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
      this.mouseArea = this.localBounds.dilatedX( 12 );
      this.touchArea = this.localBounds.dilatedX( 12 );

      // Add the indentations that are intended to convey the idea of "gripability".
      const grippyNode = new Node();
      const indentSpacing = height * 0.05;
      for ( let i = 0; i < 3; i++ ) {
        const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
        indentNode.top = i * ( indentNode.height + indentSpacing );
        grippyNode.addChild( indentNode );
      }
      grippyNode.center = this.center;
      this.addChild( grippyNode );

      const dragListener = new DragListener( {
        start: ( event, listener ) => {
          assert && assert( this.plot.getDataExists(), 'data should exist for the cursor to be draggable' );
          options.startDrag();
        },
        drag: ( event, listener ) => {
          const parentX = listener.parentPoint.x;
          let newValue = this.plot.positionToValue( parentX );

          // limit cursor to the domain of recorded values
          newValue = Utils.clamp( newValue, plot.getMinRecordedValue(), plot.getMaxRecordedValue() );
          this.plot.setCursorValue( newValue );

          options.drag();
        },
        end: ( event, listener ) => {
          options.endDrag();
        },
        tandem: options.tandem.createTandem( 'dragListener' )
      } );
      this.addInputListener( dragListener );
    }
  }

  /**
   * This node is meant to portray a small round indentation on a surface.  This is a modern user interface paradigm that
   * is intended to convey the concept of "gripability" (sp?), i.e. something that the user can click on and subsequently
   * grab.  This is meant to look somewhat 3D, much like etched borders do.
   */
  class GrippyIndentNode extends Circle {

    /**
     * @param {number} diameter
     * @param {Color} baseColor
     * @param {Object} [options]
     */
    constructor( diameter, baseColor, options ) {

      options = merge( {
        lineWidth: 0.5
      }, options );

      const baseDarkerColor = baseColor.darkerColor( 0.9 );
      const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
        baseDarkerColor.getBlue(), baseColor.getAlpha() );
      const baseLighterColor = baseColor.brighterColor( 0.9 );
      const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
        baseLighterColor.getBlue(), baseColor.getAlpha() );
      const radius = diameter / 2 - options.lineWidth;

      super( radius, {
        fill: translucentDarkerColor,
        stroke: translucentBrighterColor,
        lineWidth: options.lineWidth
      } );
    }
  }

  return griddle.register( 'XYCursorPlot', XYCursorPlot );
} );<commit_msg>update copyright dates from daily grunt work<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * An XYPlot that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 */
define( require => {
  'use strict';

  // modules
  const Circle = require( 'SCENERY/nodes/Circle' );
  const Color = require( 'SCENERY/util/Color' );
  const DragListener = require( 'SCENERY/listeners/DragListener' );
  const griddle = require( 'GRIDDLE/griddle' );
  const merge = require( 'PHET_CORE/merge' );
  const Node = require( 'SCENERY/nodes/Node' );
  const Rectangle = require( 'SCENERY/nodes/Rectangle' );
  const Tandem = require( 'TANDEM/Tandem' );
  const Utils = require( 'DOT/Utils' );
  const XYPlot = require( 'GRIDDLE/XYPlot' );

  // constants
  const WIDTH_PROPORTION = 0.013; // empirically determined
  const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
  const CURSOR_STROKE_COLOR = Color.DARK_GRAY;

  class XYCursorPlot extends XYPlot {

    constructor( options ) {

      options = merge( {

        // options passed on to the chart cursor, see ChartCursor
        cursorOptions: null,

        // phet-io
        tandem: Tandem.OPTIONAL
      }, options );

      super( options );

      // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
      this._cursorVisibleOverride = null;

      // @private {number} - value for the cursor, determines cursor positioning
      this.cursorValue = 0;

      // @private - minimum and maximum recorded value, required by the cursor to limit dragging
      this.minRecordedValue = 0;
      this.maxRecordedValue = 0;

      // @private - Keep track of listeners for each series so the listeners can be removed when the series is removed
      this.valueSeriesListenerMap = new Map();

      // @private
      this.chartCursor = new ChartCursor( this, options.cursorOptions );
      this.addChild( this.chartCursor );

      // initialize position and visibility of the cursor
      this.updateChartCursor();
    }

    /**
     * Add a series to the plot, first adding a listener that keeps track of minimum and maximum data values.
     * @public
     * @override
     *
     * @param {DynamicSeries} series
     * @param {number} scaleFactor
     */
    addSeries( series, scaleFactor ) {
      super.addSeries( series, scaleFactor );

      // when a point is added, update the min and max recorded values
      const seriesListener = () => {

        // update min/max domain of the plotted data
        this.updateMinMaxXValues();

        // if all data has been removed from the plot, update cursor visibility
        this.updateChartCursor();
      };

      // save to map so that listener can be found again for disposal
      this.valueSeriesListenerMap.set( series, seriesListener );
      series.addDynamicSeriesListener( seriesListener );
    }

    /**
     * Remove a series from the plot and dispose of the plot specific series listener.
     * @public
     *
     * @param {DynamicSeries} series - to remove
     */
    removeSeries( series ) {
      series.removeDynamicSeriesListener( this.valueSeriesListenerMap.get( series ) );
      this.valueSeriesListenerMap.delete( series );
      super.removeSeries( series );
    }

    /**
     * Set the cursor value.
     * @public
     *
     * @param {number} value
     */
    setCursorValue( value ) {
      this.cursorValue = value;
      this.updateChartCursor();
    }

    /**
     * Get the value currently under the cursor.
     * @public
     *
     * @returns {number}
     */
    getCursorValue() {
      return this.cursorValue;
    }

    /**
     * Override the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
     * described in updateChartCursorVisibility. Otherwise, visibility will equal the boolean value set here.
     * @param {boolean|null} visible
     */
    setCursorVisibleOverride( visible ) {
      assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
      this._cursorVisibleOverride = visible;
      this.updateChartCursorVisibility();
    }

    /**
     * Update the chart cursor visibility and position.
     * @private
     */
    updateChartCursor() {
      this.updateChartCursorVisibility();
      if ( this.chartCursor.isVisible() ) {
        this.updateChartCursorPos();
      }
    }

    /**
     * Update the chart cursor position.
     * @private
     */
    updateChartCursorPos() {
      const recordingStartValue = this.getMinRecordedValue();
      const recordingCurrentValue = this.cursorValue;
      this.moveChartCursorToValue( ( recordingCurrentValue - recordingStartValue ) );
    }

    /**
     * Update the chart cursor visibility. The chart cursor should be visible any time the cursor value is within
     * the recorded value range.
     *
     * @private
     */
    updateChartCursorVisibility() {
      if ( typeof this._cursorVisibleOverride === 'boolean' ) {
        this.chartCursor.setVisible( this._cursorVisibleOverride );
      }
      else {
        const valueOnChart = ( this.cursorValue - this.getMinRecordedValue() );
        const isCurrentValueOnChart = ( valueOnChart >= 0 ) && ( valueOnChart <= this.maxX );
        const dataExists = this.getDataExists();
        const chartCursorVisible = isCurrentValueOnChart && dataExists;
        this.chartCursor.setVisible( chartCursorVisible );
      }
    }

    /**
     * Get the chart value for the given position in view coordinates, relative tot the parent coordinate frame.
     * @private
     *
     * @param {number} position
     * @returns {number}
     */
    positionToValue( position ) {
      return position * ( this.maxX - this.minX ) / this.plotPath.width;
    }

    /**
     * Get the position on the chart from the provided value.
     * @private
     *
     * @param {number} value
     * @returns {number}
     */
    valueToPosition( value ) {
      return value * this.plotPath.width / ( this.maxX - this.minX );
    }

    /**
     * Move the chart cursor to the specified value.
     * @private
     *
     * @param {number} value
     */
    moveChartCursorToValue( value ) {

      // origin of cursor is at the center
      const xPosition = value * this.plotPath.width / ( this.maxX - this.minX );
      this.chartCursor.centerX = Utils.clamp( xPosition, 0, this.plotPath.width );
      this.chartCursor.centerY = this.plotPath.centerY;
    }

    /**
     * Get the minimum X data value in the data series lists. Returns zero if no data has been added yet. This value
     * is updated whenever data is added to the list.
     *
     * @returns {number}
     */
    getMinRecordedValue() {
      return !this.getDataExists() ? 0 : this.minRecordedValue;
    }

    /**
     * Get the maximum X data value in the data series lists. Returns zero if no data has been added yet.  This value
     * is updated whenever data is added to the data series list.
     *
     * @returns {number}
     */
    getMaxRecordedValue() {
      return !this.getDataExists() ? 0 : this.maxRecordedValue;
    }

    /**
     * Update the minimum/maximum plotted domain of the data recorded on this plot. This information is used
     * to determine selected cursor values. If no data is associated with this plot, extrema are represented by
     * min = infinity, max = negative infinity.
     * @private
     */
    updateMinMaxXValues() {
      this.minRecordedValue = Number.POSITIVE_INFINITY;
      this.maxRecordedValue = Number.NEGATIVE_INFINITY;
      for ( let i = 0; i < this.dataSeriesList.length; i++ ) {
        const dataSeries = this.dataSeriesList[ i ];
        for ( let j = 0; j < dataSeries.getLength(); j++ ) {
          const xValue = dataSeries.getDataPoint( j ).x;

          if ( xValue > this.maxRecordedValue ) {
            this.maxRecordedValue = xValue;
          }
          if ( xValue < this.minRecordedValue ) {
            this.minRecordedValue = xValue;
          }
        }
      }
    }
  }

  /**
   * Rectangular cursor that indicates a current or selected value on the chart.
   */
  class ChartCursor extends Rectangle {

    /**
     * @param {XYPlot} plot
     * @param {number} options
     */
    constructor( plot, options ) {

      options = merge( {
        startDrag: () => {},
        endDrag: () => {},
        drag: () => {},

        // phet-io
        tandem: Tandem.OPTIONAL
      }, options );

      const width = plot.plotPath.width * WIDTH_PROPORTION;
      const height = plot.plotPath.height;

      // Set the shape. Origin is at the center top of the rectangle.
      super( 0, -height, width, height, 0, 0, {
        cursor: 'e-resize',
        fill: CURSOR_FILL_COLOR,
        stroke: CURSOR_STROKE_COLOR,
        lineWidth: 0.4,
        lineDash: [ 4, 4 ]
      } );

      this.plot = plot;

      // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
      this.mouseArea = this.localBounds.dilatedX( 12 );
      this.touchArea = this.localBounds.dilatedX( 12 );

      // Add the indentations that are intended to convey the idea of "gripability".
      const grippyNode = new Node();
      const indentSpacing = height * 0.05;
      for ( let i = 0; i < 3; i++ ) {
        const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
        indentNode.top = i * ( indentNode.height + indentSpacing );
        grippyNode.addChild( indentNode );
      }
      grippyNode.center = this.center;
      this.addChild( grippyNode );

      const dragListener = new DragListener( {
        start: ( event, listener ) => {
          assert && assert( this.plot.getDataExists(), 'data should exist for the cursor to be draggable' );
          options.startDrag();
        },
        drag: ( event, listener ) => {
          const parentX = listener.parentPoint.x;
          let newValue = this.plot.positionToValue( parentX );

          // limit cursor to the domain of recorded values
          newValue = Utils.clamp( newValue, plot.getMinRecordedValue(), plot.getMaxRecordedValue() );
          this.plot.setCursorValue( newValue );

          options.drag();
        },
        end: ( event, listener ) => {
          options.endDrag();
        },
        tandem: options.tandem.createTandem( 'dragListener' )
      } );
      this.addInputListener( dragListener );
    }
  }

  /**
   * This node is meant to portray a small round indentation on a surface.  This is a modern user interface paradigm that
   * is intended to convey the concept of "gripability" (sp?), i.e. something that the user can click on and subsequently
   * grab.  This is meant to look somewhat 3D, much like etched borders do.
   */
  class GrippyIndentNode extends Circle {

    /**
     * @param {number} diameter
     * @param {Color} baseColor
     * @param {Object} [options]
     */
    constructor( diameter, baseColor, options ) {

      options = merge( {
        lineWidth: 0.5
      }, options );

      const baseDarkerColor = baseColor.darkerColor( 0.9 );
      const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
        baseDarkerColor.getBlue(), baseColor.getAlpha() );
      const baseLighterColor = baseColor.brighterColor( 0.9 );
      const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
        baseLighterColor.getBlue(), baseColor.getAlpha() );
      const radius = diameter / 2 - options.lineWidth;

      super( radius, {
        fill: translucentDarkerColor,
        stroke: translucentBrighterColor,
        lineWidth: options.lineWidth
      } );
    }
  }

  return griddle.register( 'XYCursorPlot', XYCursorPlot );
} );<|endoftext|>


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

/**
 * XY Data Series
 *
 * @author Sam Reid
 * @author Aadish Gupta
 */
define( function( require ) {
  'use strict';

  // modules
  var Emitter = require( 'AXON/Emitter' );
  var griddle = require( 'GRIDDLE/griddle' );
  var inherit = require( 'PHET_CORE/inherit' );

  // for the uniqueId, see this.uniqueId for information
  var instanceCount = 0;

  function XYDataSeries( options ) {

    options = _.extend( {
      color: 'black',
      lineWidth: 1,
      // size of array to initially allocate for the series, specify expected max in options for best performance
      initialSize: 1000
    }, options );

    // @public (read-only) - each instance is given a uniqueId so that it can be mapped to an XYDataSeriesNode. When
    // we can use native maps, this should no longer be necessary, see https://github.com/phetsims/griddle/issues/41
    // and https://github.com/phetsims/tasks/issues/992
    this.uniqueId = instanceCount++;

    this.cleared = new Emitter(); // @public, event emitted when the series is cleared
    this.color = options.color; // @public
    this.lineWidth = options.lineWidth; // @public
    this.listeners = []; // @private

    this.xPoints = new Array( options.initialSize ); // @private
    this.yPoints = new Array( options.initialSize ); // @private

    this.dataSeriesLength = 0; // @private, index to next available slot
  }

  griddle.register( 'XYDataSeries', XYDataSeries );

  return inherit( Object, XYDataSeries, {

    addDataSeriesListener: function( listener ) {
      this.listeners.push( listener );
    },

    removeDataSeriesListener: function( listener ) {
      var index = this.listeners.indexOf( listener );
      if ( index !== -1 ) {
        this.listeners.splice( index, 1 );
      }
    },

    addPoint: function( x, y ) {

      var index = this.dataSeriesLength;

      // point to the next slot
      this.dataSeriesLength++;

      // store the data
      this.xPoints[ index ] = x;
      this.yPoints[ index ] = y;

      // notify listeners - note that the previous data series values can be undefined in the notification
      for ( var i = 0; i < this.listeners.length; i++ ) {
        this.listeners[ i ]( x, y, this.xPoints[ index - 1 ], this.yPoints[ index - 1 ] );
      }

    },

    clear: function() {
      this.dataSeriesLength = 0;
      this.cleared.emit();
    },

    getX: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( 'No Data Point Exist at this index ' + index );
      }
      return this.xPoints[ index ];
    },

    getY: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( 'No Data Point Exist at this index ' + index );
      }
      return this.yPoints[ index ];
    },

    /**
     * @public - getter for the length.  DON'T CHANGE THIS TO AN ES5 GETTER.  That's what is was originally, and it
     * caused poor performance on iPad, see https://github.com/phetsims/neuron/issues/55.
     */
    getLength: function() {
      return this.dataSeriesLength;
    },

    /**
     * @public - getter for all the x points
     */
    getXPoints: function() {
      return this.xPoints;
    },

    /**
     * @public - getter for all the x points
     */
    getYPoints: function() {
      return this.yPoints;
    }

  } );
} );<commit_msg>promote Note to TODO, see #41<commit_after>// Copyright 2014-2017, University of Colorado Boulder

/**
 * XY Data Series
 *
 * @author Sam Reid
 * @author Aadish Gupta
 */
define( function( require ) {
  'use strict';

  // modules
  var Emitter = require( 'AXON/Emitter' );
  var griddle = require( 'GRIDDLE/griddle' );
  var inherit = require( 'PHET_CORE/inherit' );

  // for the uniqueId, see this.uniqueId for information
  var instanceCount = 0;

  function XYDataSeries( options ) {

    options = _.extend( {
      color: 'black',
      lineWidth: 1,
      // size of array to initially allocate for the series, specify expected max in options for best performance
      initialSize: 1000
    }, options );

    // @public (read-only) - each instance is given a uniqueId so that it can be mapped to an XYDataSeriesNode.
    // TODO: When we can use native maps, this should no longer be necessary, see https://github.com/phetsims/tasks/issues/992
    this.uniqueId = instanceCount++;

    this.cleared = new Emitter(); // @public, event emitted when the series is cleared
    this.color = options.color; // @public
    this.lineWidth = options.lineWidth; // @public
    this.listeners = []; // @private

    this.xPoints = new Array( options.initialSize ); // @private
    this.yPoints = new Array( options.initialSize ); // @private

    this.dataSeriesLength = 0; // @private, index to next available slot
  }

  griddle.register( 'XYDataSeries', XYDataSeries );

  return inherit( Object, XYDataSeries, {

    addDataSeriesListener: function( listener ) {
      this.listeners.push( listener );
    },

    removeDataSeriesListener: function( listener ) {
      var index = this.listeners.indexOf( listener );
      if ( index !== -1 ) {
        this.listeners.splice( index, 1 );
      }
    },

    addPoint: function( x, y ) {

      var index = this.dataSeriesLength;

      // point to the next slot
      this.dataSeriesLength++;

      // store the data
      this.xPoints[ index ] = x;
      this.yPoints[ index ] = y;

      // notify listeners - note that the previous data series values can be undefined in the notification
      for ( var i = 0; i < this.listeners.length; i++ ) {
        this.listeners[ i ]( x, y, this.xPoints[ index - 1 ], this.yPoints[ index - 1 ] );
      }

    },

    clear: function() {
      this.dataSeriesLength = 0;
      this.cleared.emit();
    },

    getX: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( 'No Data Point Exist at this index ' + index );
      }
      return this.xPoints[ index ];
    },

    getY: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( 'No Data Point Exist at this index ' + index );
      }
      return this.yPoints[ index ];
    },

    /**
     * @public - getter for the length.  DON'T CHANGE THIS TO AN ES5 GETTER.  That's what is was originally, and it
     * caused poor performance on iPad, see https://github.com/phetsims/neuron/issues/55.
     */
    getLength: function() {
      return this.dataSeriesLength;
    },

    /**
     * @public - getter for all the x points
     */
    getXPoints: function() {
      return this.xPoints;
    },

    /**
     * @public - getter for all the x points
     */
    getYPoints: function() {
      return this.yPoints;
    }

  } );
} );<|endoftext|>


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

/**
 * XY Data Series
 *
 * @author Sam Reid
 */
define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var Events = require( 'AXON/Events' );

  function XYDataSeries( options ) {

    options = _.extend( {
      color: 'black',
      maxPoints: 1000
    }, options );

    this.color = options.color; // @public
    this.maxPoints = options.maxPoints; // @private
    this.listeners = []; // @private

    //this.xPoints = [];
    this.xPoints = new Array( options.maxPoints ); // @private
    //this.yPoints = [];
    this.yPoints = new Array( options.maxPoints ); // @private

    this.dataSeriesLength = 0; // @private, index to next available slot

    Events.call( this );
  }

  return inherit( Events, XYDataSeries, {

    addDataSeriesListener: function( listener ) {
      this.listeners.push( listener );
    },

    removeDataSeriesListener: function( listener ) {
      var index = this.listeners.indexOf( listener );
      if ( index !== -1 ) {
        this.listeners.splice( index, 1 );
      }
    },

    addPoint: function( x, y ) {

      // store the data
      this.xPoints[ this.dataSeriesLength ] = x;
      this.yPoints[ this.dataSeriesLength ] = y;

      // notify listeners - note that the previous data series values can be undefined in the notification
      for ( var i = 0; i < this.listeners.length; i++ ) {
        this.listeners[ i ]( x, y, this.xPoints[ this.dataSeriesLength - 1 ], this.yPoints[ this.dataSeriesLength - 1 ] );
      }

      // point to the next slot
      this.dataSeriesLength++;
      assert && assert( this.dataSeriesLength < this.maxPoints, 'Capacity of data series exceeded' );
    },

    clear: function() {
      this.dataSeriesLength = 0;
      this.trigger( 'cleared' );
    },

    getX: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( "No Data Point Exist at this index " + index );
      }
      return this.xPoints[ index ];
    },

    getY: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( "No Data Point Exist at this index " + index );
      }
      return this.yPoints[ index ];
    },

    /**
     * @public - getter for the length.  DON'T CHANGE THIS TO AN ES5 GETTER.  That's what is was originally, and it
     * caused poor performance on iPad, see https://github.com/phetsims/neuron/issues/55.
     */
    getLength: function() {
      return this.dataSeriesLength;
    }
  } );
} );<commit_msg>eslint fix<commit_after>// Copyright 2002-2014, University of Colorado Boulder

/**
 * XY Data Series
 *
 * @author Sam Reid
 */
define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var Events = require( 'AXON/Events' );

  function XYDataSeries( options ) {

    options = _.extend( {
      color: 'black',
      maxPoints: 1000
    }, options );

    this.color = options.color; // @public
    this.maxPoints = options.maxPoints; // @private
    this.listeners = []; // @private

    //this.xPoints = [];
    this.xPoints = new Array( options.maxPoints ); // @private
    //this.yPoints = [];
    this.yPoints = new Array( options.maxPoints ); // @private

    this.dataSeriesLength = 0; // @private, index to next available slot

    Events.call( this );
  }

  return inherit( Events, XYDataSeries, {

    addDataSeriesListener: function( listener ) {
      this.listeners.push( listener );
    },

    removeDataSeriesListener: function( listener ) {
      var index = this.listeners.indexOf( listener );
      if ( index !== -1 ) {
        this.listeners.splice( index, 1 );
      }
    },

    addPoint: function( x, y ) {

      // store the data
      this.xPoints[ this.dataSeriesLength ] = x;
      this.yPoints[ this.dataSeriesLength ] = y;

      // notify listeners - note that the previous data series values can be undefined in the notification
      for ( var i = 0; i < this.listeners.length; i++ ) {
        this.listeners[ i ]( x, y, this.xPoints[ this.dataSeriesLength - 1 ], this.yPoints[ this.dataSeriesLength - 1 ] );
      }

      // point to the next slot
      this.dataSeriesLength++;
      assert && assert( this.dataSeriesLength < this.maxPoints, 'Capacity of data series exceeded' );
    },

    clear: function() {
      this.dataSeriesLength = 0;
      this.trigger( 'cleared' );
    },

    getX: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( 'No Data Point Exist at this index ' + index );
      }
      return this.xPoints[ index ];
    },

    getY: function( index ) {
      if ( index > this.dataSeriesLength - 1 ) {
        throw new Error( 'No Data Point Exist at this index ' + index );
      }
      return this.yPoints[ index ];
    },

    /**
     * @public - getter for the length.  DON'T CHANGE THIS TO AN ES5 GETTER.  That's what is was originally, and it
     * caused poor performance on iPad, see https://github.com/phetsims/neuron/issues/55.
     */
    getLength: function() {
      return this.dataSeriesLength;
    }
  } );
} );<|endoftext|>


<commit_before>var IS_HIDDEN = true;
var HIDDEN_TEXT = "Show Courses ▼";
var SHOWN_TEXT = "Hide Courses ▲";

function showHideCourses () {
  if (IS_HIDDEN){
    var courseList = $("#internalList").height();
    $( "#coursesList").velocity({height: courseList});
    $( "#showCourses").text(SHOWN_TEXT);
    IS_HIDDEN = false;
  } else {
    $( "#coursesList").velocity({height:0});
    $( "#showCourses").text(HIDDEN_TEXT);
    IS_HIDDEN = true;
  }
};

<commit_msg>fix padding<commit_after>var IS_HIDDEN = true;
var HIDDEN_TEXT = "Show Courses ▼";
var SHOWN_TEXT = "Hide Courses ▲";

function showHideCourses () {
  if (IS_HIDDEN){
    var courseList = $("#internalList").height() + 10;
    $( "#coursesList").velocity({height: courseList});
    $( "#showCourses").text(SHOWN_TEXT);
    IS_HIDDEN = false;
  } else {
    $( "#coursesList").velocity({height:0});
    $( "#showCourses").text(HIDDEN_TEXT);
    IS_HIDDEN = true;
  }
};

<|endoftext|>