<commit_before>var parseUrl = require('node-parse-url');
var net = require('net');
var utils = require("./utils.js");

function getStreamStation(url, callback) {
  var urlString = url;
  var completed = false;
  var buffer = "";
  var maxBufferSize = 100000;

  // Failure timer
  var timeout = setTimeout(function() {
    tearDown();
    return callback(new Error("Attempting to fetch station data via stream timed out."));
  }, 5000);

  var url = parseUrl(url);
  var client = new net.Socket();
  client.setTimeout(2);
  client.setEncoding('utf8');

  var port = url.port || 80;
  var getString = "GET " + url.path + " HTTP/1.0\r\n\Icy-Metadata: 1\r\nUser-Agent: Winamp 2.8\r\nhost: " + url.hostname + ":" + port + "\r\n\r\n";
  client.connect(port, url.hostname, function() {
    client.write(getString);
  });

  var dataCallback = function(response) {
    var title = null;

    // Append to the buffer and check if our title is fully included yet
    // We're looking for a string with the format of
    // StreamTitle=Artist Name - Song Name;
    buffer += response;

    var startSubstring = "StreamTitle=";
    var startPosition = buffer.indexOf(startSubstring);
    var endSubstring = ";";
    var endPosition = buffer.toString().indexOf(";", startPosition);
    if (startPosition > -1 && endPosition > startPosition) {
      tearDown();
      var titleString = buffer.substring(startPosition, endPosition);
      title = titleString.substring(13, titleString.length - 1);
      return handleTitle(title, callback);
    }

    if (buffer.length > maxBufferSize) {
      return returnError();
    }
  };

  var errorCallback = function(error) {
    if (completed) {
      return;
    }
    tearDown();
    console.log(error);
    return callback(error)
  };

  var closeCallback = function() {
    tearDown();

    var redirectUrl = handleRedirect(buffer);
    if (redirectUrl) {
      getStreamStation(redirectUrl, callback);
    }

    if (areThereErrors(buffer)) {
      return returnError();
    }

    if (completed) {
      return;
    }
  }

  function tearDown() {
    clearTimeout(timeout);

    completed = true;
    if (client != null) {
      client.destroy()
      client = null;
    }
  }

  function handleTitle(title, callback) {
    title = utils.fixTrackTitle(title)

    var station = {};
    station.title = title;
    station.fetchsource = "STREAM";
    return callback(null, station);
  }

  function handleRedirect(buffer) {
    var redirectTest = /Location: (.*)/m.exec(buffer);
    if (redirectTest) {
      // Redirect!
      var newUrl = redirectTest[1]
      console.log("Redirect: " + urlString + " to: " + newUrl);
      return newUrl
    }

    return false
  }

  function areThereErrors(buffer) {
    // If we get back HTML there's a problem
    var contentTypeTest = /Content-Type: text\/html(.*)/m.exec(buffer);
    if (contentTypeTest) {
      return true;
    }

    return false
  }

  function returnError() {
    tearDown();
    return callback(new Error("Error fetching stream"));
  }

  client.on('data', dataCallback);
  client.on('error', errorCallback);
  client.on('close', closeCallback);
}

module.exports.getStreamStation = getStreamStation;
<commit_msg>Bump up the socket timeout value<commit_after>var parseUrl = require('node-parse-url');
var net = require('net');
var utils = require("./utils.js");

function getStreamStation(url, callback) {
  var urlString = url;
  var completed = false;
  var buffer = "";
  var maxBufferSize = 100000;

  // Failure timer
  var timeout = setTimeout(function() {
    tearDown();
    return callback(new Error("Attempting to fetch station data via stream timed out."));
  }, 5000);

  var url = parseUrl(url);
  var client = new net.Socket();
  client.setTimeout(5);
  client.setEncoding('utf8');

  var port = url.port || 80;
  var getString = "GET " + url.path + " HTTP/1.0\r\n\Icy-Metadata: 1\r\nUser-Agent: Winamp 2.8\r\nhost: " + url.hostname + ":" + port + "\r\n\r\n";
  client.connect(port, url.hostname, function() {
    client.write(getString);
  });

  var dataCallback = function(response) {
    var title = null;

    // Append to the buffer and check if our title is fully included yet
    // We're looking for a string with the format of
    // StreamTitle=Artist Name - Song Name;
    buffer += response;

    var startSubstring = "StreamTitle=";
    var startPosition = buffer.indexOf(startSubstring);
    var endSubstring = ";";
    var endPosition = buffer.toString().indexOf(";", startPosition);
    if (startPosition > -1 && endPosition > startPosition) {
      tearDown();
      var titleString = buffer.substring(startPosition, endPosition);
      title = titleString.substring(13, titleString.length - 1);
      return handleTitle(title, callback);
    }

    if (buffer.length > maxBufferSize) {
      return returnError();
    }
  };

  var errorCallback = function(error) {
    if (completed) {
      return;
    }
    tearDown();
    console.log(error);
    return callback(error)
  };

  var closeCallback = function() {
    tearDown();

    var redirectUrl = handleRedirect(buffer);
    if (redirectUrl) {
      getStreamStation(redirectUrl, callback);
    }

    if (areThereErrors(buffer)) {
      return returnError();
    }

    if (completed) {
      return;
    }
  }

  function tearDown() {
    clearTimeout(timeout);

    completed = true;
    if (client != null) {
      client.destroy()
      client = null;
    }
  }

  function handleTitle(title, callback) {
    title = utils.fixTrackTitle(title)

    var station = {};
    station.title = title;
    station.fetchsource = "STREAM";
    return callback(null, station);
  }

  function handleRedirect(buffer) {
    var redirectTest = /Location: (.*)/m.exec(buffer);
    if (redirectTest) {
      // Redirect!
      var newUrl = redirectTest[1]
      console.log("Redirect: " + urlString + " to: " + newUrl);
      return newUrl
    }

    return false
  }

  function areThereErrors(buffer) {
    // If we get back HTML there's a problem
    var contentTypeTest = /Content-Type: text\/html(.*)/m.exec(buffer);
    if (contentTypeTest) {
      return true;
    }

    return false
  }

  function returnError() {
    tearDown();
    return callback(new Error("Error fetching stream"));
  }

  client.on('data', dataCallback);
  client.on('error', errorCallback);
  client.on('close', closeCallback);
}

module.exports.getStreamStation = getStreamStation;
<|endoftext|>


<commit_before>(function($) {
	// What does the is plugin do?
	$.is = $.fn.is = function() {
		var value, expression, regex;

		if (arguments.length === 2) {
			value = arguments[0];
			expression = arguments[1];
		} else {
			var nodeName = this[0].nodeName.toLowerCase();
			expression = arguments[0];

			value = nodeName === 'input' ?
				this.val() :
				nodeName === 'p' || nodeName === 'h1' || nodeName === 'h2' || nodeName === 'h3' || nodeName === 'h4' || nodeName === 'h5' || nodeName === 'h6' || nodeName === 'a' || nodeName === 'li' || nodeName === 'blockquote' || nodeName === 'pre' ||  nodeName === 'code' || nodeName === 'b' ||  nodeName === 'strong' ||  nodeName === 'em' || nodeName === 'i' || nodeName === 'div' || nodeName === 'button' || nodeName === 'textarea' || nodeName === 'span' ?
				this.html() :
				this.selector;
		}

		regex = {
			cc: /^[0-9]{16}$/,
			datetime: /^([0-2][0-9]{3})\-([0-1][0-9])\-([0-3][0-9])T([0-5][0-9])\:([0-5][0-9])\:([0-5][0-9])(Z|([\-\+]([0-1][0-9])\:00))$/,
			email: /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
			isbn: /^(?:(?=.{17}$)97[89][ -](?:[0-9]+[ -]){2}[0-9]+[ -][0-9]|97[89][0-9]{10}|(?=.{13}$)(?:[0-9]+[ -]){2}[0-9]+[ -][0-9Xx]|[0-9]{9}[0-9Xx])$/,
			latlng: /-?\d{1,3}\.\d+/,
			phone: /^(?:\+\d{3}\s?)?\d{3}[ ]?[-]?[ ]?\d{4}$/
		};

		/**
		* Tests a regular expression against a string
		* @param  {string} value
		* @param  {regexp} expression
		* @return {boolean}
		*/
		function test( value, expression ) {
			return expression.test( value.trim() );
		}

		if (regex.hasOwnProperty(expression)) {
			return test(value, regex[expression]);
		}

		if (typeof expression === 'object') {
			return test(value, expression);
		}
	};

})(jQuery);
<commit_msg>Return if nothing is in jQuery obj<commit_after>(function($) {
	// What does the is plugin do?
	$.is = $.fn.is = function() {
		if ( this.length === 0 ) return this;
		var value, expression, regex;

		if (arguments.length === 2) {
			value = arguments[0];
			expression = arguments[1];
		} else {
			var nodeName = this[0].nodeName.toLowerCase();
			expression = arguments[0];

			value = nodeName === 'input' ?
				this.val() :
				nodeName === 'p' || nodeName === 'h1' || nodeName === 'h2' || nodeName === 'h3' || nodeName === 'h4' || nodeName === 'h5' || nodeName === 'h6' || nodeName === 'a' || nodeName === 'li' || nodeName === 'blockquote' || nodeName === 'pre' ||  nodeName === 'code' || nodeName === 'b' ||  nodeName === 'strong' ||  nodeName === 'em' || nodeName === 'i' || nodeName === 'div' || nodeName === 'button' || nodeName === 'textarea' || nodeName === 'span' ?
				this.html() :
				this.selector;
		}

		regex = {
			cc: /^[0-9]{16}$/,
			datetime: /^([0-2][0-9]{3})\-([0-1][0-9])\-([0-3][0-9])T([0-5][0-9])\:([0-5][0-9])\:([0-5][0-9])(Z|([\-\+]([0-1][0-9])\:00))$/,
			email: /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
			isbn: /^(?:(?=.{17}$)97[89][ -](?:[0-9]+[ -]){2}[0-9]+[ -][0-9]|97[89][0-9]{10}|(?=.{13}$)(?:[0-9]+[ -]){2}[0-9]+[ -][0-9Xx]|[0-9]{9}[0-9Xx])$/,
			latlng: /-?\d{1,3}\.\d+/,
			phone: /^(?:\+\d{3}\s?)?\d{3}[ ]?[-]?[ ]?\d{4}$/
		};

		/**
		* Tests a regular expression against a string
		* @param  {string} value
		* @param  {regexp} expression
		* @return {boolean}
		*/
		function test( value, expression ) {
			return expression.test( value.trim() );
		}

		if (regex.hasOwnProperty(expression)) {
			return test(value, regex[expression]);
		}

		if (typeof expression === 'object') {
			return test(value, expression);
		}
	};

})(jQuery);
<|endoftext|>


<commit_before>// Include the libraries.
var phantom =   require('phantom');
var async = require('async');
var _ = require('underscore');
var go = require('asyncgo');

// The current page.
var page = null;
var pageQueue = [];
var instance = null;
var instanceQueue = [];
var loading = true;

// Create the phantom connection.
phantom.create("--web-security=false", "--ignore-ssl-errors=true", "--ssl-protocol=any", function(ph) {

  // Save the instance.
  instance = ph;

  // Empty the get instance queue.
  _.each(instanceQueue, function(queue) {
    queue(instance);
  });

  // Create the page.
  return ph.createPage(function(pg) {

    // Set the page.
    page = pg;

    // Num resources outstanding.
    var resources = 0;

    // Whether page has loaded.
    var ready = false;

    // Pass along console messages.
    page.set('onConsoleMessage', function(msg) {
      console.log('Console:' + msg);
    });

    // Log any errors.
    page.set('onResourceError', function(err) {
      console.log('ERROR: ' + err.errorString);
    });

    // Increment our outstanding resources counter.
    page.set('onResourceRequested', function() {
      if (ready) {
        loading = true;
        resources++;
      }
    });

    // Fire an event when we have received a resource.
    page.set('onResourceReceived', function(res) {
      if (ready && (res.stage == 'end') && (--resources == 0)) {
        loading = false;
      }
    });

    // Trigger when the loading has started.
    page.set('onLoadStarted', function() {
      loading = true;
    });

    // Trigger when the loading has finished.
    page.set('onLoadFinished', function() {
      loading = (resources > 0);
      ready = true;
    });

    // Empty the get page queue.
    _.each(pageQueue, function(queue) {
      queue(page);
    });
  });
});

/**
 * Returns the current page within the phantom browser.
 *
 * @param {function} callback
 *   Called with the page once it is available.
 */
var getPage = function(callback) {
  if (page) {
    callback(page);
  }
  else {
    pageQueue.push(callback);
  }
};

/**
 * TODO:
 */
var getPhantom = function(callback) {
  if (instance) {
    callback(instance);
  }
  else {
    instanceQueue.push(callback);
  }
};

/**
 * jQuery Interface class.
 *
 * @param {string} selector
 *   The selector to use for the interface.
 * @param {string} context
 *   The context to use for the interface.
 *
 * @returns {jQueryInterface}
 */
var jQueryInterface = function(selector, context) {
  this.selector = selector;
  if (typeof context == 'function') {
    this.context = null;
    this.asyncExecute(null, null, context);
  }
  else {
    this.context = context;
  }

  this.instance = -1;
  this.index = -1;
  this.length = -1;
};

/**
 *  Execute the query.
 */
jQueryInterface.prototype.asyncExecute = function(method, args, callback) {
  var self = this;

  // Get the page.
  getPage(function(page) {

    // Evaluate some javascript on this page.
    page.evaluate(function(a) {
      var query = null, retVal = {};
      if (a.instance >= 0 && typeof window.phNodes[a.instance] !== undefined) {
        query = window.phNodes[a.instance];
      }
      else {

        // See if the selector is a stored node.
        if (typeof a.selector == 'number' && typeof window.phNodes[a.selector] !== undefined) {
          a.selector = window.phNodes[a.selector];
        }

        // See if the context is a stored node.
        if (typeof a.context == 'number' && typeof window.phNodes[a.context] !== undefined) {
          a.context = window.phNodes[a.context];
        }

        // Query the new jQuery item.
        query = jQuery(a.selector, a.context);

        // Add this to the phNodes array.
        if (!window.phNodes) {
          window.phNodes = [];
        }
        window.phNodes.push(query);
        retVal.instance = (window.phNodes.length - 1);
      }

      retVal.length = query.length;

      if (a.index >= 0) {
        query = query.eq(a.index);
      }

      if (a.method) {
        if (a.args.length > 0) {
          retVal.value = query[a.method].apply(query, a.args);
        }
        else {
          retVal.value = query[a.method]();
        }
      }
      else {
        retVal.value = query.length;
      }

      // Make sure not to return the whole jquery object.
      if (typeof retVal.value.jquery === 'string') {
        retVal.value = false;
      }

      // Return the value.
      return retVal;
    }, function(retVal) {

      // Set the instance and length if they exist.
      if (retVal.instance) {
        self.instance = retVal.instance;
      }

      // Trigger the callback.
      self.length = retVal.length;
      callback.call(self, retVal.value);
    }, {
      selector: self.selector,
      context: self.context,
      instance: self.instance,
      index: self.index,
      method: method,
      args: args
    });
  });
};

/**
 * Maps the calls made to the interface to a call made within PhantomJS browser.
 */
jQueryInterface.prototype.asyncCall = function() {
  var self = this;
  var args = _.filter(_.values(arguments), function(arg) {
    return !!arg;
  });
  var method = args.shift();
  var callback = args.pop();
  context = function(val) {
    if (loading) {
      setTimeout(function() {
        context.call(self, val);
      }, 100);
    } else {
      callback.call(self, val);
    }
  }
  this.asyncExecute(method, args, context);
};

// Set the go method.
jQueryInterface.prototype.go = go;

// For each of the jQuery interfaces, add a prototype.
_.each(['add', 'addBack', 'addClass', 'after', 'ajaxComplete', 'ajaxError',
  'ajaxSend', 'ajaxStart', 'ajaxStop', 'ajaxSuccess', 'andSelf', 'animate',
  'append', 'appendTo', 'attr', 'before', 'bind', 'blur', 'change', 'children',
  'clearQueue', 'click', 'clone', 'closest', 'contents', 'css', 'data',
  'dblclick', 'delay', 'delegate', 'dequeue', 'detach', 'die', 'empty', 'end',
  'eq', 'error', 'fadeIn', 'fadeOut', 'fadeTo', 'fadeToggle', 'filter', 'find',
  'finish', 'first', 'focus', 'focusin', 'focusout', 'get', 'has', 'hasClass',
  'height', 'hide', 'hover', 'html', 'index', 'innerHeight', 'innerWidth',
  'insertAfter', 'insertBefore', 'is', 'keydown', 'keypress', 'keyup', 'last',
  'live', 'load', 'map', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove',
  'mouseout', 'mouseover', 'mouseup', 'next', 'nextAll', 'nextUntil', 'not',
  'off', 'offset', 'offsetParent', 'on', 'one', 'outerHeight', 'outerWidth',
  'parent', 'parents', 'parentsUntil', 'position', 'prepend', 'prependTo',
  'prev', 'prevAll', 'prevUntil', 'promise', 'prop', 'pushStack', 'queue',
  'ready', 'remove', 'removeAttr', 'removeClass', 'removeData', 'removeProp',
  'replaceAll', 'replaceWith', 'resize', 'scroll', 'scrollLeft', 'scrollTop',
  'select', 'serialize', 'serializeArray', 'show', 'siblings', 'size', 'slice',
  'slideDown', 'slideToggle', 'slideUp', 'stop', 'submit', 'text', 'toArray',
  'toggle', 'toggleClass', 'trigger', 'triggerHandler', 'unbind', 'undelegate',
  'unload', 'unwrap', 'val', 'width', 'wrap', 'wrapAll', 'wrapInner'
], function(method) {

  // Add the prototype for that method.
  jQueryInterface.prototype[method] = function() {
    var args = _.values(arguments);
    args.unshift(method);
    return this.asyncCall.apply(this, args);
  };
});

/**
 * Implement the each method.
 *
 * @param function callback
 *   The callback that is called for each item.
 * @param function done
 *   The function that is called when all of the items have been iterated.
 */
jQueryInterface.prototype.each = function(callback, done) {
  var self = this;

  // If the length has not yet been defined.
  if (this.length < 0) {
    this.eq(0, function() {
      if (!self.length) {
        done(true);
      }
      else {
        self.each(callback, done);
      }
    });
  }
  else {
    this.index++;
    if (this.index >= this.length) {
      done();
    }
    else {
      callback.call(this, this.index, this, function() {
        self.each(callback, done);
      });
    }
  }
};

/**
 * Define the jQuery() interface.
 *
 * @param {type} selector
 * @param {type} context
 * @returns {jQuery.Anonym$0}
 */
var jQuery = _.extend(function(selector, context) {

  // If this already is a jQueryInterface.
  if (typeof selector === 'object') {
    return selector;
  }
  else {
    // Return a new jQuery Interface.
    return new jQueryInterface(selector, context);
  }
}, {

  /**
   * Ability to change the configs.
   */
  config: {
    addJQuery: true,
    jQuery: '//code.jquery.com/jquery-1.11.1.min.js',
    site: '',
    width: 1920,
    height: 1080,
    debug: true
  },

  /**
   * Keep track when the viewport size has been set.
   */
  viewportSizeSet: false,

  /**
   * Send a debug message to the console.
   */
  debug: function(msg) {
    if (this.config.debug) {
      console.log(msg);
    }
  },

  /**
   * Wait a period of time.
   */
  wait: function(time, done) {
    var self = this;
    setTimeout(function() {
      done.call(self);
    }, time);
  },

  /**
   * Used to visit a page.
   *
   * @param {string} url
   *   The url you wish to visit.
   * @param {function} callback
   *   Called when the page is done visiting.
   */
  visit: function(url, callback) {
    var self = this;
    getPage(function(page) {

      // Set the page size if it hasn't already been set.
      if (!self.viewportSizeSet) {
        self.viewportSizeSet = true;
        page.set('viewportSize', {
          width: self.config.width,
          height: self.config.height
        }, function (result) {
          self.debug("Viewport set to: " + result.width + "x" + result.height);
        });
      }

      // Set a custom user agent if it hasn't already been set.
      if (self.config.userAgent && !self.userAgentSet) {
        self.userAgentSet = true;
        page.set('settings.userAgent', self.config.userAgent, function (result) {
          self.debug("Useragent set to: " + result);
        });
      }

      // Open the page.
      self.debug('Navigating to ' + self.config.site + url);
      page.open(self.config.site + url, function(status) {
        if (status == 'fail') {
          self.close();
          throw new Error(status);
        }
        if (self.config.addJQuery) {
          var loadJS = function() {
            if (loading) {
              setTimeout(loadJS, 100);
            }
            else {
              page.includeJs(self.config.jQuery, callback);
            }
          }
          loadJS();
        }
        else {
          self.waitForPage(callback);
        }
      });
    });
  },

  /**
   * Wait for the page to load.
   *
   * @param {type} callback
   * @returns {undefined}
   */
  waitForPage: function(callback, nowait) {
    var self = this;
    var loadWait = function() {
      setTimeout(function() {
        self.waitForPage(callback, true);
      }, 100);
    };
    if (nowait) {
      if (loading) {
        loadWait();
      }
      else {
        getPage(function(page) {
          page.evaluate(function() {
            return jQuery.isReady;
          }, function(ready) {
            if (ready) {
              callback.call(self);
            }
            else {
              loadWait();
            }
          });
        });
      }
    }
    else {
      loadWait();
    }
  },

  /**
   * Waits for an element to be present.
   */
  waitForElement: function(element, callback, nowait) {
    var self = this;
    if (!nowait) {
      this.waitForPage(function() {
        self.waitForElement(element, callback, true);
      });
    }
    else {
      var loadWait = function() {
        setTimeout(function() {
          self.waitForElement(element, callback, true);
        }, 100);
      };
      if (nowait) {
        getPage(function(page) {
          page.evaluate(function(element) {
            var element = jQuery(element);
            return ((element.length > 0) && element.is(':visible'));
          }, function(found) {
            if (found) {
              self.debug('Element ' + element + ' found');
              callback.call(self);
            }
            else {
              loadWait();
            }
          }, element);
        });
      }
      else {
        loadWait();
      }
    }
  },

  /**
   * Capture the page as an image.
   */
  capture: function(filename, done) {
    this.debug('Capturing page at ' + filename);
    getPage(function(page) {
      page.render(filename, done);
    });
  },

  /**
   * Upload File to a Form.
   */
  uploadFile: function(selector, filename, done) {
    this.debug('Upload ' + filename + ' to ' + selector);
    getPage(function(page) {
      page.uploadFile(selector, filename, done);
    });
      done();
  },

  /**
   * Close the phantom browser.
   * @returns {undefined}
   */
  close: function() {

    // Close the phantom browser instance.
    if (instance) {
      this.debug('Closing');
      instance.exit();
    }
  },

  /**
   * Prints to the console.
   */
  print: function(text, done) {
    console.log(text);
    done();
  },

  /**
   * The getPage method.
   */
  getPage: getPage,

  /**
   * The getPhantom method.
   */
  getPhantom: getPhantom,

  /**
   * The go method.
   */
  go: go
});

module.exports = jQuery;
<commit_msg>Added "ensureJqueryIsLoaded" method, fixed waitForPage issue when "jQuery" is undefined<commit_after>// Include the libraries.
var phantom =   require('phantom');
var async = require('async');
var _ = require('underscore');
var go = require('asyncgo');

// The current page.
var page = null;
var pageQueue = [];
var instance = null;
var instanceQueue = [];
var loading = true;

// Create the phantom connection.
phantom.create("--web-security=false", "--ignore-ssl-errors=true", "--ssl-protocol=any", function(ph) {

  // Save the instance.
  instance = ph;

  // Empty the get instance queue.
  _.each(instanceQueue, function(queue) {
    queue(instance);
  });

  // Create the page.
  return ph.createPage(function(pg) {

    // Set the page.
    page = pg;

    // Num resources outstanding.
    var resources = 0;

    // Whether page has loaded.
    var ready = false;

    // Pass along console messages.
    page.set('onConsoleMessage', function(msg) {
      console.log('Console:' + msg);
    });

    // Log any errors.
    page.set('onResourceError', function(err) {
      console.log('ERROR: ' + err.errorString);
    });

    // Increment our outstanding resources counter.
    page.set('onResourceRequested', function() {
      if (ready) {
        loading = true;
        resources++;
      }
    });

    // Fire an event when we have received a resource.
    page.set('onResourceReceived', function(res) {
      if (ready && (res.stage == 'end') && (--resources == 0)) {
        loading = false;
      }
    });

    // Trigger when the loading has started.
    page.set('onLoadStarted', function() {
      loading = true;
    });

    // Trigger when the loading has finished.
    page.set('onLoadFinished', function() {
      loading = (resources > 0);
      ready = true;
    });

    // Empty the get page queue.
    _.each(pageQueue, function(queue) {
      queue(page);
    });
  });
});

/**
 * Returns the current page within the phantom browser.
 *
 * @param {function} callback
 *   Called with the page once it is available.
 */
var getPage = function(callback) {
  if (page) {
    callback(page);
  }
  else {
    pageQueue.push(callback);
  }
};

/**
 * TODO:
 */
var getPhantom = function(callback) {
  if (instance) {
    callback(instance);
  }
  else {
    instanceQueue.push(callback);
  }
};

/**
 * jQuery Interface class.
 *
 * @param {string} selector
 *   The selector to use for the interface.
 * @param {string} context
 *   The context to use for the interface.
 *
 * @returns {jQueryInterface}
 */
var jQueryInterface = function(selector, context) {
  this.selector = selector;
  if (typeof context == 'function') {
    this.context = null;
    this.asyncExecute(null, null, context);
  }
  else {
    this.context = context;
  }

  this.instance = -1;
  this.index = -1;
  this.length = -1;
};

/**
 *  Execute the query.
 */
jQueryInterface.prototype.asyncExecute = function(method, args, callback) {
  var self = this;

  // Get the page.
  getPage(function(page) {

    // Evaluate some javascript on this page.
    page.evaluate(function(a) {
      var query = null, retVal = {};
      if (a.instance >= 0 && typeof window.phNodes[a.instance] !== undefined) {
        query = window.phNodes[a.instance];
      }
      else {

        // See if the selector is a stored node.
        if (typeof a.selector == 'number' && typeof window.phNodes[a.selector] !== undefined) {
          a.selector = window.phNodes[a.selector];
        }

        // See if the context is a stored node.
        if (typeof a.context == 'number' && typeof window.phNodes[a.context] !== undefined) {
          a.context = window.phNodes[a.context];
        }

        // Query the new jQuery item.
        query = jQuery(a.selector, a.context);

        // Add this to the phNodes array.
        if (!window.phNodes) {
          window.phNodes = [];
        }
        window.phNodes.push(query);
        retVal.instance = (window.phNodes.length - 1);
      }

      retVal.length = query.length;

      if (a.index >= 0) {
        query = query.eq(a.index);
      }

      if (a.method) {
        if (a.args.length > 0) {
          retVal.value = query[a.method].apply(query, a.args);
        }
        else {
          retVal.value = query[a.method]();
        }
      }
      else {
        retVal.value = query.length;
      }

      // Make sure not to return the whole jquery object.
      if (typeof retVal.value.jquery === 'string') {
        retVal.value = false;
      }

      // Return the value.
      return retVal;
    }, function(retVal) {

      // Set the instance and length if they exist.
      if (retVal.instance) {
        self.instance = retVal.instance;
      }

      // Trigger the callback.
      self.length = retVal.length;
      callback.call(self, retVal.value);
    }, {
      selector: self.selector,
      context: self.context,
      instance: self.instance,
      index: self.index,
      method: method,
      args: args
    });
  });
};

/**
 * Maps the calls made to the interface to a call made within PhantomJS browser.
 */
jQueryInterface.prototype.asyncCall = function() {
  var self = this;
  var args = _.filter(_.values(arguments), function(arg) {
    return !!arg;
  });
  var method = args.shift();
  var callback = args.pop();
  context = function(val) {
    if (loading) {
      setTimeout(function() {
        context.call(self, val);
      }, 100);
    } else {
      callback.call(self, val);
    }
  }
  this.asyncExecute(method, args, context);
};

// Set the go method.
jQueryInterface.prototype.go = go;

// For each of the jQuery interfaces, add a prototype.
_.each(['add', 'addBack', 'addClass', 'after', 'ajaxComplete', 'ajaxError',
  'ajaxSend', 'ajaxStart', 'ajaxStop', 'ajaxSuccess', 'andSelf', 'animate',
  'append', 'appendTo', 'attr', 'before', 'bind', 'blur', 'change', 'children',
  'clearQueue', 'click', 'clone', 'closest', 'contents', 'css', 'data',
  'dblclick', 'delay', 'delegate', 'dequeue', 'detach', 'die', 'empty', 'end',
  'eq', 'error', 'fadeIn', 'fadeOut', 'fadeTo', 'fadeToggle', 'filter', 'find',
  'finish', 'first', 'focus', 'focusin', 'focusout', 'get', 'has', 'hasClass',
  'height', 'hide', 'hover', 'html', 'index', 'innerHeight', 'innerWidth',
  'insertAfter', 'insertBefore', 'is', 'keydown', 'keypress', 'keyup', 'last',
  'live', 'load', 'map', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove',
  'mouseout', 'mouseover', 'mouseup', 'next', 'nextAll', 'nextUntil', 'not',
  'off', 'offset', 'offsetParent', 'on', 'one', 'outerHeight', 'outerWidth',
  'parent', 'parents', 'parentsUntil', 'position', 'prepend', 'prependTo',
  'prev', 'prevAll', 'prevUntil', 'promise', 'prop', 'pushStack', 'queue',
  'ready', 'remove', 'removeAttr', 'removeClass', 'removeData', 'removeProp',
  'replaceAll', 'replaceWith', 'resize', 'scroll', 'scrollLeft', 'scrollTop',
  'select', 'serialize', 'serializeArray', 'show', 'siblings', 'size', 'slice',
  'slideDown', 'slideToggle', 'slideUp', 'stop', 'submit', 'text', 'toArray',
  'toggle', 'toggleClass', 'trigger', 'triggerHandler', 'unbind', 'undelegate',
  'unload', 'unwrap', 'val', 'width', 'wrap', 'wrapAll', 'wrapInner'
], function(method) {

  // Add the prototype for that method.
  jQueryInterface.prototype[method] = function() {
    var args = _.values(arguments);
    args.unshift(method);
    return this.asyncCall.apply(this, args);
  };
});

/**
 * Implement the each method.
 *
 * @param function callback
 *   The callback that is called for each item.
 * @param function done
 *   The function that is called when all of the items have been iterated.
 */
jQueryInterface.prototype.each = function(callback, done) {
  var self = this;

  // If the length has not yet been defined.
  if (this.length < 0) {
    this.eq(0, function() {
      if (!self.length) {
        done(true);
      }
      else {
        self.each(callback, done);
      }
    });
  }
  else {
    this.index++;
    if (this.index >= this.length) {
      done();
    }
    else {
      callback.call(this, this.index, this, function() {
        self.each(callback, done);
      });
    }
  }
};

/**
 * Define the jQuery() interface.
 *
 * @param {type} selector
 * @param {type} context
 * @returns {jQuery.Anonym$0}
 */
var jQuery = _.extend(function(selector, context) {

  // If this already is a jQueryInterface.
  if (typeof selector === 'object') {
    return selector;
  }
  else {
    // Return a new jQuery Interface.
    return new jQueryInterface(selector, context);
  }
}, {

  /**
   * Ability to change the configs.
   */
  config: {
    addJQuery: true,
    jQuery: '//code.jquery.com/jquery-1.11.1.min.js',
    site: '',
    width: 1920,
    height: 1080,
    debug: true
  },

  /**
   * Keep track when the viewport size has been set.
   */
  viewportSizeSet: false,

  /**
   * Send a debug message to the console.
   */
  debug: function(msg) {
    if (this.config.debug) {
      console.log(msg);
    }
  },

  /**
   * Wait a period of time.
   */
  wait: function(time, done) {
    var self = this;
    setTimeout(function() {
      done.call(self);
    }, time);
  },

  /**
   * Include's jQuery to page if "jQuery" is undefined
   * @param {function} callback
   *   Called when the jQuery script is included.
   */
  ensureJqueryIsLoaded: function (callback) {
    var self = this;
    if(!self.config.addJQuery)
    {
      return callback();
    }
    getPage(function(page) {
      page.evaluate(function() {
        return !!window.jQuery;
      }, function(jQueryIsPresent) {
        if(jQueryIsPresent) {
          return callback();
        }
        var loadJS = function() {
          if (loading) {
            setTimeout(loadJS, 100);
          }
          else {
            page.includeJs(self.config.jQuery, function() {
              callback();
              callback = _.identity;
            });
          }
        }
        loadJS();
      });
    });
  },

  /**
   * Used to visit a page.
   *
   * @param {string} url
   *   The url you wish to visit.
   * @param {function} callback
   *   Called when the page is done visiting.
   */
  visit: function(url, callback) {
    var self = this;
    getPage(function(page) {

      // Set the page size if it hasn't already been set.
      if (!self.viewportSizeSet) {
        self.viewportSizeSet = true;
        page.set('viewportSize', {
          width: self.config.width,
          height: self.config.height
        }, function (result) {
          self.debug("Viewport set to: " + result.width + "x" + result.height);
        });
      }

      // Set a custom user agent if it hasn't already been set.
      if (self.config.userAgent && !self.userAgentSet) {
        self.userAgentSet = true;
        page.set('settings.userAgent', self.config.userAgent, function (result) {
          self.debug("Useragent set to: " + result);
        });
      }

      // Open the page.
      self.debug('Navigating to ' + self.config.site + url);
      page.open(self.config.site + url, function(status) {
        if (status == 'fail') {
          self.close();
          throw new Error(status);
        }
        if (self.config.addJQuery) {
          self.ensureJqueryIsLoaded(callback);
        }
        else {
          self.waitForPage(callback);
        }
      });
    });
  },

  /**
   * Wait for the page to load.
   *
   * @param {type} callback
   * @returns {undefined}
   */
  waitForPage: function(callback, nowait) {
    var self = this;
    var loadWait = function() {
      setTimeout(function() {
        self.waitForPage(callback, true);
      }, 100);
    };
    if (nowait) {
      if (loading) {
        loadWait();
      }
      else {
        self.ensureJqueryIsLoaded(function() {
          getPage(function(page) {
            page.evaluate(function() {
              return jQuery.isReady;
            }, function(ready) {
              if (ready) {
                callback.call(self);
              }
              else {
                loadWait();
              }
            });
          });
        });
      }
    }
    else {
      loadWait();
    }
  },

  /**
   * Waits for an element to be present.
   */
  waitForElement: function(element, callback, nowait) {
    var self = this;
    if (!nowait) {
      this.waitForPage(function() {
        self.waitForElement(element, callback, true);
      });
    }
    else {
      var loadWait = function() {
        setTimeout(function() {
          self.waitForElement(element, callback, true);
        }, 100);
      };
      if (nowait) {
        getPage(function(page) {
          page.evaluate(function(element) {
            var element = jQuery(element);
            return ((element.length > 0) && element.is(':visible'));
          }, function(found) {
            if (found) {
              self.debug('Element ' + element + ' found');
              callback.call(self);
            }
            else {
              loadWait();
            }
          }, element);
        });
      }
      else {
        loadWait();
      }
    }
  },

  /**
   * Capture the page as an image.
   */
  capture: function(filename, done) {
    this.debug('Capturing page at ' + filename);
    getPage(function(page) {
      page.render(filename, done);
    });
  },

  /**
   * Upload File to a Form.
   */
  uploadFile: function(selector, filename, done) {
    this.debug('Upload ' + filename + ' to ' + selector);
    getPage(function(page) {
      page.uploadFile(selector, filename, done);
    });
      done();
  },

  /**
   * Close the phantom browser.
   * @returns {undefined}
   */
  close: function() {

    // Close the phantom browser instance.
    if (instance) {
      this.debug('Closing');
      instance.exit();
    }
  },

  /**
   * Prints to the console.
   */
  print: function(text, done) {
    console.log(text);
    done();
  },

  /**
   * The getPage method.
   */
  getPage: getPage,

  /**
   * The getPhantom method.
   */
  getPhantom: getPhantom,

  /**
   * The go method.
   */
  go: go
});

module.exports = jQuery;
<|endoftext|>


<commit_before>/* -*- mode: javascript; c-basic-offset: 4; indent-tabs-mode: nil -*- */

//
// Dalliance Genome Explorer
// (c) Thomas Down 2006-2010
//
// domui.js: SVG UI components
//

"use strict";

if (typeof(require) !== 'undefined') {
    var browser = require('./cbrowser');
    var Browser = browser.Browser;

    var utils = require('./utils');
    var makeElement = utils.makeElement;
    var removeChildren = utils.removeChildren;
}

Browser.prototype.removeAllPopups = function() {
    for (var pi = 0; pi < this.popups.length; ++pi) {
        var popup = this.popups[pi];
        if (popup.onClose) {
            try {
                popup.onClose();
            } catch (ex) {
                console.log(ex);
            }
        }
        this.hPopupHolder.removeChild(popup.component);
    }
    this.popups = [];
}

Browser.prototype.makeTooltip = function(ele, text)
{
    ele.title = text;
}

Browser.prototype.popit = function(ev, name, ele, opts)
{
    var thisB = this;
    if (!opts)
        opts = {};
    if (!ev)
        ev = {};

    var width = opts.width || 200;

    var mx, my;

    if (ev.clientX) {
        var mx =  ev.clientX, my = ev.clientY;
    } else {
        mx = 500; my= 50;
    }
    mx +=  document.documentElement.scrollLeft || document.body.scrollLeft;
    my +=  document.documentElement.scrollTop || document.body.scrollTop;
    var winWidth = window.innerWidth;

    var top = my;
    var left = Math.min(mx - (width/2) - 4, (winWidth - width - 30));

    var popup = makeElement('div');
    popup.className = 'popover fade ' + (ev.clientX ? 'bottom ' : '') + 'in';
    popup.style.display = 'block';
    popup.style.position = 'absolute';
    popup.style.top = '' + top + 'px';
    popup.style.left = '' + left + 'px';
    popup.style.width = width + 'px';
    if (width > 276) {
        // HACK Bootstrappification...
        popup.style.maxWidth = width + 'px';
    }

    popup.appendChild(makeElement('div', null, {className: 'arrow'}));

    if (name) {
        var closeButton = makeElement('button', '', {className: 'close'});
        closeButton.innerHTML = '×'

        closeButton.addEventListener('mouseover', function(ev) {
            closeButton.style.color = 'red';
        }, false);
        closeButton.addEventListener('mouseout', function(ev) {
            closeButton.style.color = 'black';
        }, false);
        closeButton.addEventListener('click', function(ev) {
            ev.preventDefault(); ev.stopPropagation();
            thisB.removeAllPopups();
        }, false);
        var tbar = makeElement('h4', [makeElement('span', name, null, {maxWidth: '200px'}), closeButton], {/*className: 'popover-title' */}, {paddingLeft: '10px', paddingRight: '10px'});

        if ( !opts.disableDrag ) {
            var dragOX, dragOY;
            var moveHandler, upHandler;
            moveHandler = function(ev) {
                ev.stopPropagation(); ev.preventDefault();
                left = left + (ev.clientX - dragOX);
                if (left < 8) {
                    left = 8;
                } if (left > (winWidth - width - 32)) {
                    left = (winWidth - width - 26);
                }
                top = top + (ev.clientY - dragOY);
                top = Math.max(10, top);
                popup.style.top = '' + top + 'px';
                popup.style.left = '' + Math.min(left, (winWidth - width - 10)) + 'px';
                dragOX = ev.clientX; dragOY = ev.clientY;
            }
            upHandler = function(ev) {
                ev.stopPropagation(); ev.preventDefault();
                window.removeEventListener('mousemove', moveHandler, false);
                window.removeEventListener('mouseup', upHandler, false);
            }
            tbar.addEventListener('mousedown', function(ev) {
                ev.preventDefault(); ev.stopPropagation();
                dragOX = ev.clientX; dragOY = ev.clientY;
                window.addEventListener('mousemove', moveHandler, false);
                window.addEventListener('mouseup', upHandler, false);
            }, false);
        }

        popup.appendChild(tbar);
    }

    popup.appendChild(makeElement('div', ele, {className: 'popover-content'}, {
        padding: '0px'
    }));
    this.hPopupHolder.appendChild(popup);

    this.popups.push({
        component: popup,
        onClose: opts.onClose
    });
}

function makeTreeTableSection(title, content, visible) {

    var showHide = function(ev) {
        ev.preventDefault(); ev.stopPropagation();
        visible = !visible;
        update();
    };

    var ttButton = makeElement('i');
    function update() {
        if (visible) {
            ttButton.className = 'fa fa-caret-down';
            content.style.display = 'table';
        } else {
            ttButton.className = 'fa fa-caret-right';
            content.style.display = 'none';
        }
    }
    update();

    ttButton.addEventListener('click', showHide, false);

    var heading = makeElement('h6', [ttButton, ' ', title], {}, {display: 'block', background: 'gray', color: 'white', width: '100%', padding: '5px 2px', margin: '0px'});
    heading.addEventListener('click', showHide, false);

    return makeElement('div', [heading, content], {});
}

if (typeof(module) !== 'undefined') {
    module.exports = {
        makeTreeTableSection: makeTreeTableSection
    };
}
<commit_msg>Move the definition of showHide<commit_after>/* -*- mode: javascript; c-basic-offset: 4; indent-tabs-mode: nil -*- */

//
// Dalliance Genome Explorer
// (c) Thomas Down 2006-2010
//
// domui.js: SVG UI components
//

"use strict";

if (typeof(require) !== 'undefined') {
    var browser = require('./cbrowser');
    var Browser = browser.Browser;

    var utils = require('./utils');
    var makeElement = utils.makeElement;
    var removeChildren = utils.removeChildren;
}

Browser.prototype.removeAllPopups = function() {
    for (var pi = 0; pi < this.popups.length; ++pi) {
        var popup = this.popups[pi];
        if (popup.onClose) {
            try {
                popup.onClose();
            } catch (ex) {
                console.log(ex);
            }
        }
        this.hPopupHolder.removeChild(popup.component);
    }
    this.popups = [];
}

Browser.prototype.makeTooltip = function(ele, text)
{
    ele.title = text;
}

Browser.prototype.popit = function(ev, name, ele, opts)
{
    var thisB = this;
    if (!opts)
        opts = {};
    if (!ev)
        ev = {};

    var width = opts.width || 200;

    var mx, my;

    if (ev.clientX) {
        var mx =  ev.clientX, my = ev.clientY;
    } else {
        mx = 500; my= 50;
    }
    mx +=  document.documentElement.scrollLeft || document.body.scrollLeft;
    my +=  document.documentElement.scrollTop || document.body.scrollTop;
    var winWidth = window.innerWidth;

    var top = my;
    var left = Math.min(mx - (width/2) - 4, (winWidth - width - 30));

    var popup = makeElement('div');
    popup.className = 'popover fade ' + (ev.clientX ? 'bottom ' : '') + 'in';
    popup.style.display = 'block';
    popup.style.position = 'absolute';
    popup.style.top = '' + top + 'px';
    popup.style.left = '' + left + 'px';
    popup.style.width = width + 'px';
    if (width > 276) {
        // HACK Bootstrappification...
        popup.style.maxWidth = width + 'px';
    }

    popup.appendChild(makeElement('div', null, {className: 'arrow'}));

    if (name) {
        var closeButton = makeElement('button', '', {className: 'close'});
        closeButton.innerHTML = '×'

        closeButton.addEventListener('mouseover', function(ev) {
            closeButton.style.color = 'red';
        }, false);
        closeButton.addEventListener('mouseout', function(ev) {
            closeButton.style.color = 'black';
        }, false);
        closeButton.addEventListener('click', function(ev) {
            ev.preventDefault(); ev.stopPropagation();
            thisB.removeAllPopups();
        }, false);
        var tbar = makeElement('h4', [makeElement('span', name, null, {maxWidth: '200px'}), closeButton], {/*className: 'popover-title' */}, {paddingLeft: '10px', paddingRight: '10px'});

        if ( !opts.disableDrag ) {
            var dragOX, dragOY;
            var moveHandler, upHandler;
            moveHandler = function(ev) {
                ev.stopPropagation(); ev.preventDefault();
                left = left + (ev.clientX - dragOX);
                if (left < 8) {
                    left = 8;
                } if (left > (winWidth - width - 32)) {
                    left = (winWidth - width - 26);
                }
                top = top + (ev.clientY - dragOY);
                top = Math.max(10, top);
                popup.style.top = '' + top + 'px';
                popup.style.left = '' + Math.min(left, (winWidth - width - 10)) + 'px';
                dragOX = ev.clientX; dragOY = ev.clientY;
            }
            upHandler = function(ev) {
                ev.stopPropagation(); ev.preventDefault();
                window.removeEventListener('mousemove', moveHandler, false);
                window.removeEventListener('mouseup', upHandler, false);
            }
            tbar.addEventListener('mousedown', function(ev) {
                ev.preventDefault(); ev.stopPropagation();
                dragOX = ev.clientX; dragOY = ev.clientY;
                window.addEventListener('mousemove', moveHandler, false);
                window.addEventListener('mouseup', upHandler, false);
            }, false);
        }

        popup.appendChild(tbar);
    }

    popup.appendChild(makeElement('div', ele, {className: 'popover-content'}, {
        padding: '0px'
    }));
    this.hPopupHolder.appendChild(popup);

    this.popups.push({
        component: popup,
        onClose: opts.onClose
    });
}

function makeTreeTableSection(title, content, visible) {

    var ttButton = makeElement('i');
    function update() {
        if (visible) {
            ttButton.className = 'fa fa-caret-down';
            content.style.display = 'table';
        } else {
            ttButton.className = 'fa fa-caret-right';
            content.style.display = 'none';
        }
    }
    update();

    var showHide = function(ev) {
        ev.preventDefault(); ev.stopPropagation();
        visible = !visible;
        update();
    };

    ttButton.addEventListener('click', showHide, false);

    var heading = makeElement('h6', [ttButton, ' ', title], {}, {display: 'block', background: 'gray', color: 'white', width: '100%', padding: '5px 2px', margin: '0px'});
    heading.addEventListener('click', showHide, false);

    return makeElement('div', [heading, content], {});
}

if (typeof(module) !== 'undefined') {
    module.exports = {
        makeTreeTableSection: makeTreeTableSection
    };
}
<|endoftext|>


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

/**
 * A scenery node that is used to represent a draggable Measuring Tape.
 * It contains a tip and a base that can be dragged separately,
 * with a text indicating the measurement.
 * The motion of the measuring tape can be confined by drag bounds.
 * It assumes that the position of this node is set to (0,0) in the parent Node.
 *
 * @author Vasily Shakhov (Mlearner)
 * @author Siddhartha Chinthapally (ActualConcepts)
 * @author Aaron Davis (PhET)
 * @author Martin Veillette (Berea College)
 */
define( function( require ) {
  'use strict';

  // modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Line = require( 'SCENERY/nodes/Line' );
  var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var measuringTapeImage = require( 'image!SCENERY_PHET/measuringTape.png' );

  /**
   * Constructor for the measuring Tape
   * @param {Property.<Object>} unitsProperty - it has two fields, (1) name <string> and (2) multiplier <number>, eg. {name: 'cm', multiplier: 100},
   * @param {Property.<boolean>} isVisibleProperty
   * @param {Object} [options]
   * @constructor
   */
  function MeasuringTape( unitsProperty, isVisibleProperty, options ) {
    var measuringTape = this;

    Node.call( this );
    options = _.extend( {
      basePositionProperty: new Property( new Vector2( 40, 40 ) ), // base Position in view coordinates (rightBottom position of the measuring tape image)
      unrolledTapeDistance: 1, // in model coordinates
      angle: 0.0, // angle of the tape in radians, recall that in the view, a positive angle means clockwise rotation.
      textPosition: new Vector2( 0, 30 ), // position of the text relative to center of the base image in view units
      modelViewTransform: ModelViewTransform2.createIdentity(),
      dragBounds: Bounds2.EVERYTHING,// bounds for the measuring tape (in the parent Node Coordinates reference frame), default value is no (effective) bounds
      scaleProperty: new Property( 1 ), // scale the apparent length of the unrolled Tape, without changing the measurement, analogous to a zoom factor
      significantFigures: 1, // number of significant figures in the length measurement
      textColor: 'white', // color of the length measurement and unit
      textFont: new PhetFont( { size: 16, weight: 'bold' } ), // font for the measurement text
      baseScale: 0.8, // control the size of the measuringTape Image (the base)
      lineColor: 'gray', // color of the tapeline itself
      tapeLineWidth: 2, // linewidth of the tape line
      tipCircleColor: 'rgba(0,0,0,0.1)', // color of the circle at the tip
      tipCircleRadius: 10, // radius of the circle on the tip
      crosshairColor: 'rgb(224, 95, 32)', // orange, color of the two crosshairs
      crosshairSize: 5, // size of the crosshairs in scenery coordinates ( measured from center)
      crosshairLineWidth: 2, // linewidth of the crosshairs
      isBaseCrosshairRotating: true, // do crosshairs rotate around their own axis to line up with the tapeline
      isTipCrosshairRotating: true // do crosshairs rotate around their own axis to line up with the tapeline
    }, options );

    assert && assert( Math.abs( options.modelViewTransform.modelToViewDeltaX( 1 ) ) === Math.abs( options.modelViewTransform.modelToViewDeltaY( 1 ) ), 'The y and x scale factor are not identical' );
    this.modelToViewScale = options.modelViewTransform.modelToViewDeltaX( 1 ); // private

    this.significantFigures = options.significantFigures; // @private
    this.unitsProperty = unitsProperty; // @private
    this.isVisibleProperty = isVisibleProperty; // @private
    this.scaleProperty = options.scaleProperty; // @private
    this.tipToBaseDistance = options.unrolledTapeDistance; // @private

    this.basePositionProperty = options.basePositionProperty;
    var tapeDistance = options.modelViewTransform.modelToViewDeltaX( options.unrolledTapeDistance );

    this.tipPositionProperty = new Property( options.basePositionProperty.value.plus( Vector2.createPolar( tapeDistance, options.angle ) ) );

    var crosshairShape = new Shape().
      moveTo( -options.crosshairSize, 0 ).
      moveTo( -options.crosshairSize, 0 ).
      lineTo( options.crosshairSize, 0 ).
      moveTo( 0, -options.crosshairSize ).
      lineTo( 0, options.crosshairSize );

    var baseCrosshair = new Path( crosshairShape, {
      stroke: options.crosshairColor,
      lineWidth: options.crosshairLineWidth
    } );

    var tipCrosshair = new Path( crosshairShape, {
      stroke: options.crosshairColor,
      lineWidth: options.crosshairLineWidth
    } );

    var tipCircle = new Circle( options.tipCircleRadius, { fill: options.tipCircleColor } );

    var baseImage = new Image( measuringTapeImage, {
      scale: options.baseScale,
      cursor: 'pointer'
    } );

    // create tapeline (running from one crosshair to the other)
    var tapeLine = new Line( this.basePositionProperty.value, this.tipPositionProperty.value, {
      stroke: options.lineColor,
      lineWidth: options.tapeLineWidth
    } );

    // add tipCrosshair and tipCircle to the tip
    var tip = new Node( { children: [ tipCircle, tipCrosshair ], cursor: 'pointer' } );

    // create text
    this.labelText = new Text( measuringTape.getText(), {
      font: options.textFont,
      fill: options.textColor
    } );

    // expand the area for touch
    tip.touchArea = tip.localBounds.dilatedXY( 10, 10 );
    baseImage.touchArea = baseImage.localBounds.dilatedXY( 10, 10 );

    this.addChild( tapeLine ); // tapeline going from one crosshair to the other
    this.addChild( baseCrosshair ); // crosshair near the base, (set at basePosition)
    this.addChild( baseImage ); // base of the measuring tape
    this.addChild( this.labelText ); // text
    this.addChild( tip ); // crosshair and circle at the tip (set at tipPosition)

    var startOffset;
    baseImage.addInputListener( new SimpleDragHandler( {
        allowTouchSnag: true,
        start: function( event, trail ) {
          startOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).minus( measuringTape.basePositionProperty.value );
        },

        drag: function( event ) {
          var parentPoint = event.currentTarget.globalToParentPoint( event.pointer.point ).minus( startOffset );
          var constrainedLocation = constrainBounds( parentPoint, options.dragBounds );

          // translation of the basePosition (subject to the constraining bounds)
          var translationDelta = constrainedLocation.minus( measuringTape.basePositionProperty.value );

          measuringTape.basePositionProperty.value = constrainedLocation;

          // translate the position of the tip if it is not being dragged
          if ( !isDraggingTip ) {
            measuringTape.tipPositionProperty.value = measuringTape.tipPositionProperty.value.plus( translationDelta );
          }
        }

      } )
    );

    // when the user is not holding onto the tip, dragging the body will also drag the tip
    var isDraggingTip = false;

    // init drag and drop for tip
    tip.addInputListener( new SimpleDragHandler( {
      allowTouchSnag: true,

      start: function( event, trail ) {
        isDraggingTip = true;
      },

      translate: function( translationParams ) {
        measuringTape.tipPositionProperty.value = measuringTape.tipPositionProperty.value.plus( translationParams.delta );
      },

      end: function( event, trail ) {
        isDraggingTip = false;
      }
    } ) );

    // link the positions of base and tip to the scenery nodes such as crosshair, tip, labelText, tapeLine, baseImage
    Property.multilink( [ this.basePositionProperty, this.tipPositionProperty ], function( basePosition, tipPosition ) {
      // calculate the orientation and change of orientation of the Measuring tape
      var oldAngle = baseImage.getRotation();
      var angle = Math.atan2( tipPosition.y - basePosition.y, tipPosition.x - basePosition.x );
      var deltaAngle = angle - oldAngle;

      // set position of the tip and the base crosshair
      baseCrosshair.center = basePosition;
      tip.center = tipPosition;

      // in order to avoid all kind of geometrical issues with position, let's reset the baseImage upright and then set its position and rotation
      baseImage.setRotation( 0 );
      baseImage.rightBottom = basePosition;
      baseImage.rotateAround( basePosition, angle );

      // reset the text
      measuringTape.tipToBaseDistance = tipPosition.distance( basePosition );
      measuringTape.labelText.setText( measuringTape.getText() );
      measuringTape.labelText.centerTop = baseImage.center.plus( options.textPosition.times( options.baseScale ) );

      // reposition the tapeline
      tapeLine.setLine( basePosition.x, basePosition.y, tipPosition.x, tipPosition.y );

      // rotate the crosshairs
      if ( options.isTipCrosshairRotating ) {
        tip.rotateAround( tip.center, deltaAngle );
      }
      if ( options.isBaseCrosshairRotating ) {
        baseCrosshair.rotateAround( baseCrosshair.center, deltaAngle );
      }

    } );

    // @private
    this.isVisiblePropertyObserver = function( isVisible ) {
      measuringTape.visible = isVisible;
    };
    this.isVisibleProperty.link( this.isVisiblePropertyObserver ); // must be unlinked in dispose

    // @private set Text on on labelText
    this.unitsPropertyObserver = function() {
      measuringTape.labelText.setText( measuringTape.getText() );
    };
    // link change of units to the text
    this.unitsProperty.link( this.unitsPropertyObserver ); // must be unlinked in dispose

    // @private length of the unrolled tape scales with the scaleProperty (but text stays the same).
    this.scalePropertyObserver = function( scale, oldScale ) {
      // make sure that the oldScale exists, if not set to 1.
      if ( oldScale === null ) {
        oldScale = 1;
      }
      // update the position of the tip
      var displacementVector = measuringTape.tipPositionProperty.value.minus( measuringTape.basePositionProperty.value );
      var scaledDisplacementVector = displacementVector.timesScalar( scale / oldScale );
      measuringTape.tipPositionProperty.value = measuringTape.basePositionProperty.value.plus( scaledDisplacementVector );
    };
    // scaleProperty is analogous to a zoom in/zoom out function
    this.scaleProperty.link( this.scalePropertyObserver ); // must be unlinked in dispose

    /**
     * Constrains a point to some bounds.
     *
     * @param {Vector2} point
     * @param {Bounds2} bounds
     * @returns {Vector2}
     */
    function constrainBounds( point, bounds ) {
      if ( _.isUndefined( bounds ) || bounds.containsPoint( point ) ) {
        return point;
      }
      else {
        var xConstrained = Math.max( Math.min( point.x, bounds.maxX ), bounds.minX );
        var yConstrained = Math.max( Math.min( point.y, bounds.maxY ), bounds.minY );
        return new Vector2( xConstrained, yConstrained );
      }
    }

    this.mutate( options );
  }

  return inherit( Node, MeasuringTape, {
    /**
     * reset the MeasuringTape to its initial configuration
     * @public
     */
    reset: function() {
      this.basePositionProperty.reset();
      this.tipPositionProperty.reset();
    },

    /**
     * returns a readout of the current measurement
     * @public
     * @returns {string}
     */
    getText: function() {
      return Util.toFixed( this.unitsProperty.value.multiplier * this.tipToBaseDistance / this.modelToViewScale / this.scaleProperty.value,
          this.significantFigures ) + ' ' + this.unitsProperty.value.name;
    },

    // Ensures that this node is eligible for GC.
    dispose: function() {
      this.isVisibleProperty.unlink( this.isVisiblePropertyObserver );
      this.unitsProperty.unlink( this.unitsPropertyObserver );
      this.scaleProperty.unlink( this.scalePropertyObserver );
    },

    /**
     * Set the color of the text label
     * @param {Color} color
     */
    setTextColor: function( color ) {
      this.labelText.fill = color;
    }

  } );
} );

<commit_msg>include getter and setter for the textColor of the measuringTape<commit_after>// Copyright 2002-2014, University of Colorado Boulder

/**
 * A scenery node that is used to represent a draggable Measuring Tape.
 * It contains a tip and a base that can be dragged separately,
 * with a text indicating the measurement.
 * The motion of the measuring tape can be confined by drag bounds.
 * It assumes that the position of this node is set to (0,0) in the parent Node.
 *
 * @author Vasily Shakhov (Mlearner)
 * @author Siddhartha Chinthapally (ActualConcepts)
 * @author Aaron Davis (PhET)
 * @author Martin Veillette (Berea College)
 */
define( function( require ) {
  'use strict';

  // modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Line = require( 'SCENERY/nodes/Line' );
  var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var measuringTapeImage = require( 'image!SCENERY_PHET/measuringTape.png' );

  /**
   * Constructor for the measuring Tape
   * @param {Property.<Object>} unitsProperty - it has two fields, (1) name <string> and (2) multiplier <number>, eg. {name: 'cm', multiplier: 100},
   * @param {Property.<boolean>} isVisibleProperty
   * @param {Object} [options]
   * @constructor
   */
  function MeasuringTape( unitsProperty, isVisibleProperty, options ) {
    var measuringTape = this;

    Node.call( this );
    options = _.extend( {
      basePositionProperty: new Property( new Vector2( 40, 40 ) ), // base Position in view coordinates (rightBottom position of the measuring tape image)
      unrolledTapeDistance: 1, // in model coordinates
      angle: 0.0, // angle of the tape in radians, recall that in the view, a positive angle means clockwise rotation.
      textPosition: new Vector2( 0, 30 ), // position of the text relative to center of the base image in view units
      modelViewTransform: ModelViewTransform2.createIdentity(),
      dragBounds: Bounds2.EVERYTHING,// bounds for the measuring tape (in the parent Node Coordinates reference frame), default value is no (effective) bounds
      scaleProperty: new Property( 1 ), // scale the apparent length of the unrolled Tape, without changing the measurement, analogous to a zoom factor
      significantFigures: 1, // number of significant figures in the length measurement
      textColor: 'white', // color of the length measurement and unit
      textFont: new PhetFont( { size: 16, weight: 'bold' } ), // font for the measurement text
      baseScale: 0.8, // control the size of the measuringTape Image (the base)
      lineColor: 'gray', // color of the tapeline itself
      tapeLineWidth: 2, // linewidth of the tape line
      tipCircleColor: 'rgba(0,0,0,0.1)', // color of the circle at the tip
      tipCircleRadius: 10, // radius of the circle on the tip
      crosshairColor: 'rgb(224, 95, 32)', // orange, color of the two crosshairs
      crosshairSize: 5, // size of the crosshairs in scenery coordinates ( measured from center)
      crosshairLineWidth: 2, // linewidth of the crosshairs
      isBaseCrosshairRotating: true, // do crosshairs rotate around their own axis to line up with the tapeline
      isTipCrosshairRotating: true // do crosshairs rotate around their own axis to line up with the tapeline
    }, options );

    assert && assert( Math.abs( options.modelViewTransform.modelToViewDeltaX( 1 ) ) === Math.abs( options.modelViewTransform.modelToViewDeltaY( 1 ) ), 'The y and x scale factor are not identical' );
    this.modelToViewScale = options.modelViewTransform.modelToViewDeltaX( 1 ); // private

    this.significantFigures = options.significantFigures; // @private
    this.unitsProperty = unitsProperty; // @private
    this.isVisibleProperty = isVisibleProperty; // @private
    this.scaleProperty = options.scaleProperty; // @private
    this.tipToBaseDistance = options.unrolledTapeDistance; // @private

    this.basePositionProperty = options.basePositionProperty;
    var tapeDistance = options.modelViewTransform.modelToViewDeltaX( options.unrolledTapeDistance );

    this.tipPositionProperty = new Property( options.basePositionProperty.value.plus( Vector2.createPolar( tapeDistance, options.angle ) ) );

    var crosshairShape = new Shape().
      moveTo( -options.crosshairSize, 0 ).
      moveTo( -options.crosshairSize, 0 ).
      lineTo( options.crosshairSize, 0 ).
      moveTo( 0, -options.crosshairSize ).
      lineTo( 0, options.crosshairSize );

    var baseCrosshair = new Path( crosshairShape, {
      stroke: options.crosshairColor,
      lineWidth: options.crosshairLineWidth
    } );

    var tipCrosshair = new Path( crosshairShape, {
      stroke: options.crosshairColor,
      lineWidth: options.crosshairLineWidth
    } );

    var tipCircle = new Circle( options.tipCircleRadius, { fill: options.tipCircleColor } );

    var baseImage = new Image( measuringTapeImage, {
      scale: options.baseScale,
      cursor: 'pointer'
    } );

    // create tapeline (running from one crosshair to the other)
    var tapeLine = new Line( this.basePositionProperty.value, this.tipPositionProperty.value, {
      stroke: options.lineColor,
      lineWidth: options.tapeLineWidth
    } );

    // add tipCrosshair and tipCircle to the tip
    var tip = new Node( { children: [ tipCircle, tipCrosshair ], cursor: 'pointer' } );

    // create text
    // @public
    this.labelText = new Text( measuringTape.getText(), {
      font: options.textFont,
      fill: options.textColor
    } );

    // expand the area for touch
    tip.touchArea = tip.localBounds.dilatedXY( 10, 10 );
    baseImage.touchArea = baseImage.localBounds.dilatedXY( 10, 10 );

    this.addChild( tapeLine ); // tapeline going from one crosshair to the other
    this.addChild( baseCrosshair ); // crosshair near the base, (set at basePosition)
    this.addChild( baseImage ); // base of the measuring tape
    this.addChild( this.labelText ); // text
    this.addChild( tip ); // crosshair and circle at the tip (set at tipPosition)

    var startOffset;
    baseImage.addInputListener( new SimpleDragHandler( {
        allowTouchSnag: true,
        start: function( event, trail ) {
          startOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).minus( measuringTape.basePositionProperty.value );
        },

        drag: function( event ) {
          var parentPoint = event.currentTarget.globalToParentPoint( event.pointer.point ).minus( startOffset );
          var constrainedLocation = constrainBounds( parentPoint, options.dragBounds );

          // translation of the basePosition (subject to the constraining bounds)
          var translationDelta = constrainedLocation.minus( measuringTape.basePositionProperty.value );

          measuringTape.basePositionProperty.value = constrainedLocation;

          // translate the position of the tip if it is not being dragged
          if ( !isDraggingTip ) {
            measuringTape.tipPositionProperty.value = measuringTape.tipPositionProperty.value.plus( translationDelta );
          }
        }

      } )
    );

    // when the user is not holding onto the tip, dragging the body will also drag the tip
    var isDraggingTip = false;

    // init drag and drop for tip
    tip.addInputListener( new SimpleDragHandler( {
      allowTouchSnag: true,

      start: function( event, trail ) {
        isDraggingTip = true;
      },

      translate: function( translationParams ) {
        measuringTape.tipPositionProperty.value = measuringTape.tipPositionProperty.value.plus( translationParams.delta );
      },

      end: function( event, trail ) {
        isDraggingTip = false;
      }
    } ) );

    // link the positions of base and tip to the scenery nodes such as crosshair, tip, labelText, tapeLine, baseImage
    Property.multilink( [ this.basePositionProperty, this.tipPositionProperty ], function( basePosition, tipPosition ) {
      // calculate the orientation and change of orientation of the Measuring tape
      var oldAngle = baseImage.getRotation();
      var angle = Math.atan2( tipPosition.y - basePosition.y, tipPosition.x - basePosition.x );
      var deltaAngle = angle - oldAngle;

      // set position of the tip and the base crosshair
      baseCrosshair.center = basePosition;
      tip.center = tipPosition;

      // in order to avoid all kind of geometrical issues with position, let's reset the baseImage upright and then set its position and rotation
      baseImage.setRotation( 0 );
      baseImage.rightBottom = basePosition;
      baseImage.rotateAround( basePosition, angle );

      // reset the text
      measuringTape.tipToBaseDistance = tipPosition.distance( basePosition );
      measuringTape.labelText.setText( measuringTape.getText() );
      measuringTape.labelText.centerTop = baseImage.center.plus( options.textPosition.times( options.baseScale ) );

      // reposition the tapeline
      tapeLine.setLine( basePosition.x, basePosition.y, tipPosition.x, tipPosition.y );

      // rotate the crosshairs
      if ( options.isTipCrosshairRotating ) {
        tip.rotateAround( tip.center, deltaAngle );
      }
      if ( options.isBaseCrosshairRotating ) {
        baseCrosshair.rotateAround( baseCrosshair.center, deltaAngle );
      }

    } );

    // @private
    this.isVisiblePropertyObserver = function( isVisible ) {
      measuringTape.visible = isVisible;
    };
    this.isVisibleProperty.link( this.isVisiblePropertyObserver ); // must be unlinked in dispose

    // @private set Text on on labelText
    this.unitsPropertyObserver = function() {
      measuringTape.labelText.setText( measuringTape.getText() );
    };
    // link change of units to the text
    this.unitsProperty.link( this.unitsPropertyObserver ); // must be unlinked in dispose

    // @private length of the unrolled tape scales with the scaleProperty (but text stays the same).
    this.scalePropertyObserver = function( scale, oldScale ) {
      // make sure that the oldScale exists, if not set to 1.
      if ( oldScale === null ) {
        oldScale = 1;
      }
      // update the position of the tip
      var displacementVector = measuringTape.tipPositionProperty.value.minus( measuringTape.basePositionProperty.value );
      var scaledDisplacementVector = displacementVector.timesScalar( scale / oldScale );
      measuringTape.tipPositionProperty.value = measuringTape.basePositionProperty.value.plus( scaledDisplacementVector );
    };
    // scaleProperty is analogous to a zoom in/zoom out function
    this.scaleProperty.link( this.scalePropertyObserver ); // must be unlinked in dispose

    /**
     * Constrains a point to some bounds.
     *
     * @param {Vector2} point
     * @param {Bounds2} bounds
     * @returns {Vector2}
     */
    function constrainBounds( point, bounds ) {
      if ( _.isUndefined( bounds ) || bounds.containsPoint( point ) ) {
        return point;
      }
      else {
        var xConstrained = Math.max( Math.min( point.x, bounds.maxX ), bounds.minX );
        var yConstrained = Math.max( Math.min( point.y, bounds.maxY ), bounds.minY );
        return new Vector2( xConstrained, yConstrained );
      }
    }

    this.mutate( options );
  }

  return inherit( Node, MeasuringTape, {
    /**
     * reset the MeasuringTape to its initial configuration
     * @public
     */
    reset: function() {
      this.basePositionProperty.reset();
      this.tipPositionProperty.reset();
    },

    //TODO: is is necesseray to have this method now that labelText is not longer a var
    /**
     * returns a readout of the current measurement
     * @public
     * @returns {string}
     */
    getText: function() {
      return Util.toFixed( this.unitsProperty.value.multiplier * this.tipToBaseDistance / this.modelToViewScale / this.scaleProperty.value,
          this.significantFigures ) + ' ' + this.unitsProperty.value.name;
    },

    // Ensures that this node is eligible for GC.
    dispose: function() {
      this.isVisibleProperty.unlink( this.isVisiblePropertyObserver );
      this.unitsProperty.unlink( this.unitsPropertyObserver );
      this.scaleProperty.unlink( this.scalePropertyObserver );
    },

    /**
     * Set the color of the text label
     * @param {string||Color} color
     */
    setTextColor: function( color ) {
      this.labelText.fill = color;
    },

    // ES5 getter and setter for the textColor
    set textColor( value ) { this.setTextColor( value ); },
    get textColor() { return this.labelText.fill; }

  } );
} );

<|endoftext|>


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

/**
 * The navigation bar at the bottom of the screen.
 * For a single-tab sim, it shows the name of the sim at the left and the PhET Logo and options menu at the right.
 * For a multi-tab sim, it shows icons for all of the other tabs, with the tab name at the left and the PhET Logo and options menu at the right.
 *
 * @author Sam Reid
 */
define( function( require ) {
  "use strict";

  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var VBox = require( 'SCENERY/nodes/VBox' );
  var Text = require( 'SCENERY/nodes/Text' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var PanelNode = require( 'SUN/PanelNode' );
  var BoundsNode = require( 'SUN/BoundsNode' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var inherit = require( 'PHET_CORE/inherit' );
  var SimPopupMenu = require( 'JOIST/SimPopupMenu' );
  var Font = require( 'SCENERY/util/Font' );
  var Shape = require( 'KITE/Shape' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );

  var createHighlight = function( width, height ) {
    var leftBar = new Path( {shape: Shape.lineSegment( 0, 0, 0, height ), lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    var rightBar = new Path( {shape: Shape.lineSegment( width, 0, width, height ), lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    return new Node( {children: [leftBar, rightBar], visible: false} );
  };

  var createHighlightListener = function( node ) {
    return {//Highlight a button when mousing over it
      over: function( event ) { if ( event.pointer.isMouse ) {node.visible = true;} },
      out: function( event ) { if ( event.pointer.isMouse ) {node.visible = false;} }
    };
  };

  /**
   * Create a nav bar.  Layout assumes all of the tab widths are the same.
   * @param sim
   * @param tabs
   * @param model
   * @constructor
   */
  function NavigationBar( sim, tabs, model ) {
    var navigationBar = this;
    this.tabs = tabs;

    this.navBarHeight = 40;
    this.navBarScale = 1;
    this.navBarWidth = 768;

    Node.call( this, {renderer: 'svg'} );
    this.background = new Rectangle( 0, 0, 0, 0, {fill: 'black'} );
    this.addChild( this.background );

    var fontSize = 36;

    var phetLabel = new Text( "PhET", {fontSize: fontSize, fill: 'yellow'} );
    var optionsButton = new BoundsNode( new FontAwesomeNode( 'reorder', {fill: '#fff'} ), {cursor: 'pointer'} );

    var optionsHighlight = createHighlight( optionsButton.width + 6, optionsButton.height + 5 );
    optionsHighlight.bottom = optionsButton.bottom + 3;
    optionsHighlight.x = -3;
    optionsButton.addChild( optionsHighlight );

    //Creating the popup menu dynamically (when needed) causes a temporary black screen on the iPad (perhaps because of canvas accurate text bounds)
    var simPopupMenu = new SimPopupMenu( sim );
    var optionButtonPressed = function() {
      simPopupMenu.x = navigationBar.navBarWidth - simPopupMenu.width - 2;
      simPopupMenu.y = window.innerHeight - simPopupMenu.height - navigationBar.height / 2 + 4;
      var overlayScene = sim.createAndAddOverlay( simPopupMenu );
      overlayScene.addInputListener( {down: function() {
        sim.removeOverlay( overlayScene );
      }} );
    };

    // mousedown or touchstart (pointer pressed down over the node)
    optionsButton.addPeer( '<input type="button">', {click: optionButtonPressed, tabIndex: 101} );
    optionsButton.addInputListener( { down: optionButtonPressed} );
    optionsButton.addInputListener( createHighlightListener( optionsHighlight ) );

    this.phetLabelAndButton = new HBox( {spacing: 10, children: [phetLabel, optionsButton]} );
    this.addChild( this.phetLabelAndButton );

    this.titleLabel = new Text( sim.name, {fontSize: 18, fill: 'white'} );

    //Create the nodes to be used for the tab icons
    var index = 0;

    var selectedFont = new Font( { size: 10, weight: 'bold'} );
    var normalFont = new Font( { size: 10 } );

    this.addChild( this.titleLabel );

    if ( tabs.length > 1 ) {

      var iconAndTextArray = _.map( tabs, function( tab ) {
        var icon = new Node( {children: [tab.icon], scale: 25 / tab.icon.height} );
        var text = new Text( tab.name, { fill: 'white', visible: true} );

        //Put a panel around it to extend it horizontally so there is some distance from the highlight region to the text and some distance between adjacent texts.
        var textPanel = new PanelNode( text, {
          fill: null,
          stroke: null,
          lineWidth: 1, // width of the background border
          xMargin: 4,
          yMargin: 0,
          cornerRadius: 0, // radius of the rounded corners on the background
          resize: false // dynamically resize when content bounds change?
        } );
        var iconAndText = new VBox( {children: [icon, textPanel]} );
        iconAndText.icon = icon;
        iconAndText.text = text;
        iconAndText.textPanel = textPanel;
        iconAndText.index = index++;
        iconAndText.tab = tab;

        //On initialization and when the tab changes, update the size of the icons and the layout of the icons and text
        model.tabIndexProperty.link( function( tabIndex ) {
          var selected = iconAndText.index === tabIndex;
          iconAndText.text.fill = selected ? 'yellow' : 'white';
          iconAndText.text.font = selected ? selectedFont : normalFont;
          iconAndText.opacity = selected ? 1.0 : 0.5;
        } );

        return iconAndText;
      } );

      //Make all of the icons the same size so they will have equal hit areas and equal spacing
      var maxWidth = _.max( iconAndTextArray,function( iconAndText ) {return iconAndText.width;} ).width;
      var maxHeight = _.max( iconAndTextArray,function( iconAndText ) {return iconAndText.height;} ).height;

      this.buttonArray = iconAndTextArray.map( function( iconAndText ) {
        //Background area for layout and hit region
        var rectangle = new Rectangle( 0, 0, maxWidth, maxHeight );
        var highlight = createHighlight( maxWidth, maxHeight );
        iconAndText.centerX = maxWidth / 2;
        iconAndText.top = 0;
        var button = new Node( {children: [ rectangle, highlight, iconAndText]} );

        //In the nav bar, don't show highlight or cursor pointer when over a tab icon that is already selected
        model.tabIndexProperty.link( function( tabIndex ) { button.cursor = tabIndex === iconAndText.index ? 'default' : 'pointer'; } );

        var pressListener = function() {
          model.tabIndex = iconAndText.index;
          model.showHomeScreen = false;
        };
        button.addInputListener( {
          down: pressListener,

          //Highlight a button when mousing over it
          over: function( event ) {
            if ( event.pointer.isMouse ) {
              highlight.visible = true;
            }
          },
          out: function( event ) {
            if ( event.pointer.isMouse ) {
              highlight.visible = false;
            }
          }
        } );

        button.addPeer( '<input type="button">', {click: pressListener, tabIndex: 99} );
        return  button;
      } );

      //TODO: This spacing is not always necessary, it depends on the relative width
      //TODO:   of the icon vs the text, if all of the buttons have the same dimensions.
      //TODO:   Currently this solves the simple case where all of the text is shorter than all of the icons (like in Build an Atom)
      var maxIconWidth = _.max( iconAndTextArray,function( i ) {return i.icon.width;} ).icon.width;
      var maxButtonWidth = _.max( iconAndTextArray,function( i ) {return i.width;} ).width;

      this.buttonHBox = new HBox( {spacing: maxIconWidth === maxButtonWidth ? 20 : 0, children: this.buttonArray} );
      this.addChild( this.buttonHBox );

      //add the home icon
      this.homeIcon = new BoundsNode( new FontAwesomeNode( 'home', {fill: '#fff'} ), {cursor: 'pointer'} );
      var homeHighlight = createHighlight( this.homeIcon.width + 6, this.homeIcon.height + 5 );
      homeHighlight.bottom = this.homeIcon.bottom + 3;
      homeHighlight.x = -3;
      this.homeIcon.addChild( homeHighlight );
      this.homeIcon.addInputListener( { down: function() { model.showHomeScreen = true; }} );
      this.homeIcon.addInputListener( createHighlightListener( homeHighlight ) );
      this.homeIcon.addPeer( '<input type="button">', {click: function() {model.showHomeScreen = true;}, tabIndex: 100} );
      this.addChild( this.homeIcon );
    }
  }

  inherit( Node, NavigationBar, {relayout: function() {
    var navigationBar = this;
    navigationBar.background.rectHeight = this.navBarHeight;
    navigationBar.background.rectWidth = this.navBarWidth;
    var tabIndex = navigationBar.tabIndex;

    if ( this.buttonHBox ) {
      this.buttonHBox.setScaleMagnitude( navigationBar.navBarScale );
    }

    this.titleLabel.setScaleMagnitude( this.navBarScale );
    this.titleLabel.centerY = this.navBarHeight / 2;
    this.titleLabel.left = 10;

    //Lay out the components from left to right
    if ( this.tabs.length !== 1 ) {

      //put the center right in the middle
      this.buttonHBox.centerX = this.navBarWidth / 2;
      this.buttonHBox.top = 2;

      navigationBar.homeIcon.setScaleMagnitude( this.navBarScale );
      navigationBar.homeIcon.bottom = this.navBarHeight;
      navigationBar.homeIcon.left = navigationBar.buttonHBox.right + 15;
    }
    this.phetLabelAndButton.setScaleMagnitude( this.navBarScale );
    this.phetLabelAndButton.right = this.navBarWidth - 5;
    this.phetLabelAndButton.centerY = this.navBarHeight / 2;
  },
    layout: function( scale, width, height, windowHeight ) {
      this.navBarScale = scale;
      this.navBarWidth = width;
      this.navBarHeight = height;
      this.relayout();
    }} );

  return NavigationBar;
} );
<commit_msg>In the nav bar, combined the phet text and phet options button<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * The navigation bar at the bottom of the screen.
 * For a single-tab sim, it shows the name of the sim at the left and the PhET Logo and options menu at the right.
 * For a multi-tab sim, it shows icons for all of the other tabs, with the tab name at the left and the PhET Logo and options menu at the right.
 *
 * @author Sam Reid
 */
define( function( require ) {
  "use strict";

  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var VBox = require( 'SCENERY/nodes/VBox' );
  var Text = require( 'SCENERY/nodes/Text' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var PanelNode = require( 'SUN/PanelNode' );
  var BoundsNode = require( 'SUN/BoundsNode' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var inherit = require( 'PHET_CORE/inherit' );
  var SimPopupMenu = require( 'JOIST/SimPopupMenu' );
  var Font = require( 'SCENERY/util/Font' );
  var Shape = require( 'KITE/Shape' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );

  var createHighlight = function( width, height ) {
    var leftBar = new Path( {shape: Shape.lineSegment( 0, 0, 0, height ), lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    var rightBar = new Path( {shape: Shape.lineSegment( width, 0, width, height ), lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    return new Node( {children: [leftBar, rightBar], visible: false} );
  };

  var createHighlightListener = function( node ) {
    return {//Highlight a button when mousing over it
      over: function( event ) { if ( event.pointer.isMouse ) {node.visible = true;} },
      out: function( event ) { if ( event.pointer.isMouse ) {node.visible = false;} }
    };
  };

  /**
   * Create a nav bar.  Layout assumes all of the tab widths are the same.
   * @param sim
   * @param tabs
   * @param model
   * @constructor
   */
  function NavigationBar( sim, tabs, model ) {
    var navigationBar = this;
    this.tabs = tabs;

    this.navBarHeight = 40;
    this.navBarScale = 1;
    this.navBarWidth = 768;

    Node.call( this, {renderer: 'svg'} );
    this.background = new Rectangle( 0, 0, 0, 0, {fill: 'black'} );
    this.addChild( this.background );

    var fontSize = 36;

    var phetLabel = new Text( "PhET", {fontSize: fontSize, fill: 'yellow'} );
    var optionsButton = new BoundsNode( new FontAwesomeNode( 'reorder', {fill: '#fff'} ) );

    this.phetLabelAndButton = new HBox( {spacing: 10, children: [phetLabel, optionsButton], cursor: 'pointer'} );

    var optionsHighlight = createHighlight( this.phetLabelAndButton.width + 6, this.phetLabelAndButton.height - 2 );
    optionsHighlight.bottom = optionsButton.bottom + 3;
    optionsHighlight.x = -3;
    this.phetLabelAndButton.addChild( optionsHighlight );

    //Creating the popup menu dynamically (when needed) causes a temporary black screen on the iPad (perhaps because of canvas accurate text bounds)
    var simPopupMenu = new SimPopupMenu( sim );
    var optionButtonPressed = function() {
      simPopupMenu.x = navigationBar.navBarWidth - simPopupMenu.width - 2;
      simPopupMenu.y = window.innerHeight - simPopupMenu.height - navigationBar.height / 2 + 4;
      var overlayScene = sim.createAndAddOverlay( simPopupMenu );
      overlayScene.addInputListener( {down: function() {
        sim.removeOverlay( overlayScene );
      }} );
    };

    // mousedown or touchstart (pointer pressed down over the node)
    this.phetLabelAndButton.addPeer( '<input type="button">', {click: optionButtonPressed, tabIndex: 101} );
    this.phetLabelAndButton.addInputListener( { down: optionButtonPressed} );
    this.phetLabelAndButton.addInputListener( createHighlightListener( optionsHighlight ) );

    this.addChild( this.phetLabelAndButton );

    this.titleLabel = new Text( sim.name, {fontSize: 18, fill: 'white'} );

    //Create the nodes to be used for the tab icons
    var index = 0;

    var selectedFont = new Font( { size: 10, weight: 'bold'} );
    var normalFont = new Font( { size: 10 } );

    this.addChild( this.titleLabel );

    if ( tabs.length > 1 ) {

      var iconAndTextArray = _.map( tabs, function( tab ) {
        var icon = new Node( {children: [tab.icon], scale: 25 / tab.icon.height} );
        var text = new Text( tab.name, { fill: 'white', visible: true} );

        //Put a panel around it to extend it horizontally so there is some distance from the highlight region to the text and some distance between adjacent texts.
        var textPanel = new PanelNode( text, {
          fill: null,
          stroke: null,
          lineWidth: 1, // width of the background border
          xMargin: 4,
          yMargin: 0,
          cornerRadius: 0, // radius of the rounded corners on the background
          resize: false // dynamically resize when content bounds change?
        } );
        var iconAndText = new VBox( {children: [icon, textPanel]} );
        iconAndText.icon = icon;
        iconAndText.text = text;
        iconAndText.textPanel = textPanel;
        iconAndText.index = index++;
        iconAndText.tab = tab;

        //On initialization and when the tab changes, update the size of the icons and the layout of the icons and text
        model.tabIndexProperty.link( function( tabIndex ) {
          var selected = iconAndText.index === tabIndex;
          iconAndText.text.fill = selected ? 'yellow' : 'white';
          iconAndText.text.font = selected ? selectedFont : normalFont;
          iconAndText.opacity = selected ? 1.0 : 0.5;
        } );

        return iconAndText;
      } );

      //Make all of the icons the same size so they will have equal hit areas and equal spacing
      var maxWidth = _.max( iconAndTextArray,function( iconAndText ) {return iconAndText.width;} ).width;
      var maxHeight = _.max( iconAndTextArray,function( iconAndText ) {return iconAndText.height;} ).height;

      this.buttonArray = iconAndTextArray.map( function( iconAndText ) {
        //Background area for layout and hit region
        var rectangle = new Rectangle( 0, 0, maxWidth, maxHeight );
        var highlight = createHighlight( maxWidth, maxHeight );
        iconAndText.centerX = maxWidth / 2;
        iconAndText.top = 0;
        var button = new Node( {children: [ rectangle, highlight, iconAndText]} );

        //In the nav bar, don't show highlight or cursor pointer when over a tab icon that is already selected
        model.tabIndexProperty.link( function( tabIndex ) { button.cursor = tabIndex === iconAndText.index ? 'default' : 'pointer'; } );

        var pressListener = function() {
          model.tabIndex = iconAndText.index;
          model.showHomeScreen = false;
        };
        button.addInputListener( {
          down: pressListener,

          //Highlight a button when mousing over it
          over: function( event ) {
            if ( event.pointer.isMouse ) {
              highlight.visible = true;
            }
          },
          out: function( event ) {
            if ( event.pointer.isMouse ) {
              highlight.visible = false;
            }
          }
        } );

        button.addPeer( '<input type="button">', {click: pressListener, tabIndex: 99} );
        return  button;
      } );

      //TODO: This spacing is not always necessary, it depends on the relative width
      //TODO:   of the icon vs the text, if all of the buttons have the same dimensions.
      //TODO:   Currently this solves the simple case where all of the text is shorter than all of the icons (like in Build an Atom)
      var maxIconWidth = _.max( iconAndTextArray,function( i ) {return i.icon.width;} ).icon.width;
      var maxButtonWidth = _.max( iconAndTextArray,function( i ) {return i.width;} ).width;

      this.buttonHBox = new HBox( {spacing: maxIconWidth === maxButtonWidth ? 20 : 0, children: this.buttonArray} );
      this.addChild( this.buttonHBox );

      //add the home icon
      this.homeIcon = new BoundsNode( new FontAwesomeNode( 'home', {fill: '#fff'} ), {cursor: 'pointer'} );
      var homeHighlight = createHighlight( this.homeIcon.width + 6, this.homeIcon.height + 5 );
      homeHighlight.bottom = this.homeIcon.bottom + 3;
      homeHighlight.x = -3;
      this.homeIcon.addChild( homeHighlight );
      this.homeIcon.addInputListener( { down: function() { model.showHomeScreen = true; }} );
      this.homeIcon.addInputListener( createHighlightListener( homeHighlight ) );
      this.homeIcon.addPeer( '<input type="button">', {click: function() {model.showHomeScreen = true;}, tabIndex: 100} );
      this.addChild( this.homeIcon );
    }
  }

  inherit( Node, NavigationBar, {relayout: function() {
    var navigationBar = this;
    navigationBar.background.rectHeight = this.navBarHeight;
    navigationBar.background.rectWidth = this.navBarWidth;
    var tabIndex = navigationBar.tabIndex;

    if ( this.buttonHBox ) {
      this.buttonHBox.setScaleMagnitude( navigationBar.navBarScale );
    }

    this.titleLabel.setScaleMagnitude( this.navBarScale );
    this.titleLabel.centerY = this.navBarHeight / 2;
    this.titleLabel.left = 10;

    //Lay out the components from left to right
    if ( this.tabs.length !== 1 ) {

      //put the center right in the middle
      this.buttonHBox.centerX = this.navBarWidth / 2;
      this.buttonHBox.top = 2;

      navigationBar.homeIcon.setScaleMagnitude( this.navBarScale );
      navigationBar.homeIcon.bottom = this.navBarHeight;
      navigationBar.homeIcon.left = navigationBar.buttonHBox.right + 15;
    }
    this.phetLabelAndButton.setScaleMagnitude( this.navBarScale );
    this.phetLabelAndButton.right = this.navBarWidth - 5;
    this.phetLabelAndButton.centerY = this.navBarHeight / 2;
  },
    layout: function( scale, width, height, windowHeight ) {
      this.navBarScale = scale;
      this.navBarWidth = width;
      this.navBarHeight = height;
      this.relayout();
    }} );

  return NavigationBar;
} );
<|endoftext|>