<commit_before>var fs = require('fs');
var path = require('path');
var gonzales = require('gonzales-pe');

var NodeType = {
  ATRULE_RQ: 'atrulerq',
  ARGUMENTS: 'arguments',
  BLOCK: 'block',
  COMMENT: 'singlelineComment',
  DECLARATION: 'declaration',
  DELIMITER_D: 'declarationDelimiter',
  DELIMITER_P: 'propertyDelimiter',
  EXTEND: 'extend',
  FUNCTION: 'function',
  IDENTIFIER: 'ident',
  INCLUDE: 'include',
  INTERPOLATION: 'interpolation',
  LOOP: 'loop',
  MIXIN: 'mixin',
  PARENTHESIS: 'parentheses',
  PROPERTY: 'property',
  RULESET: 'ruleset',
  SELECTOR: 'selector',
  SIMPLE_SELECTOR: 'simpleSelector',
  STYLE_SHEET: 'stylesheet',
  VALUE: 'value',
  VARIABLE: 'variable'
};

var reducerOptions = ['varsFile', 'themeFile', 'themeData', 'templateOpen', 'templateClose', 'templateSnakeCase', 'outputStyle'];

function Thematic(ast, opts) {
  this.ast = ast;
  this.cwd = opts.cwd || process.cwd();
  this.selector = [];
  this.vars = {};
  this.locals = {};
  this.extend = {};
  this.mixin = {};

  // Extend valid option names onto this object:
  for (var i=0; i < reducerOptions.length; i++) {
    var attr = reducerOptions[i];
    if (opts.hasOwnProperty(attr)) {
      this[attr] = opts[attr];
    }
  }

  // Resolve variables file path:
  if (!this.varsFile) throw 'No variables file specified.';
  if (!path.isAbsolute(this.varsFile)) {
    this.varsFile = path.resolve(this.cwd, this.varsFile);
  }

  // Write all theme variables into the reducer vars cache:
  this.varsText = fs.readFileSync(this.varsFile, 'utf-8');
  var pattern = /(\$[^\s:]+)[\s:]/g;
  var match = pattern.exec(this.varsText);

  while (match) {
    this.vars[ match[1] ] = 1;
    match = pattern.exec(this.varsText);
  }
}

Thematic.prototype = {
  varsFile: null,
  _template: false,
  templateOpen: '<%=',
  templateClose: '%>',
  outputStyle: 'nested',

  // List of removable node types:
  removeableTypes: [
    NodeType.DECLARATION,
    NodeType.EXTEND,
    NodeType.INCLUDE,
    NodeType.LOOP,
    NodeType.MIXIN,
    NodeType.RULESET
  ],

  // Checks if a type is removable:
  // constructs and caches types hash on first lookup.
  isRemovable: function(type) {
    return this.removeableTypes.indexOf(type) != -1;
  },

  /**
  * Drops a node from the tree:
  * the node's content is replaced by a single-line comment.
  */
  dropNode: function(node, desc) {
    node.content = ' ' + (desc || node.type);
    node.type = NodeType.COMMENT;
    return false;
  },

  /**
  * Collects local variable definitions that may
  * derrive from an override variable:
  * Ex: `@each $c in $keep-color`
  */
  setLocals: function(node) {
    var locals = {};
    var keep = false;
    var loop = node.toString().split('\n')[0];
    var pattern = /(\$[\w-]+)/g;
    var match = pattern.exec(loop);

    while (match) {
      locals[ match[1] ] = 1;
      keep = this.vars.hasOwnProperty(match[1]) || keep;
      match = pattern.exec(loop);
    }

    // Only keep these locals if there was a valid override within the loop:
    this.locals = keep ? locals : {};
  },

  /**
   * Methodology for recursively filtering a Sass abstract syntax tree (AST).
   * Each node is explored for relevant variables, and then pruned if irrelevant.
   * @param { object } node: a Gonzales Node object from a parsed AST.
   * @param { array } ancestry: an array of node-type strings prepresenting the current search path.
   * @return { boolean } returns true if the search branch should be retained.
   */
  reduceNode: function(node, parent) {
    var keep = false;

    // Track selector heirarchy while drilling into the tree:
    if (node.type === NodeType.SELECTOR) {
      this.selector.push(node.toString().trim());
    }

    // Track derivitive locals created by control-flow loops:
    else if (node.type === NodeType.LOOP) {
      this.setLocals(node);
    }

    // Keep variable declarations and overrides:
    else if (node.type === NodeType.VARIABLE && parent) {
      // Test if the var belongs to the set of override vars:
      var overrideVar = this.vars.hasOwnProperty(node.toString());
      // Test if the variable is keepable (includes local derivatives)
      var keepableVar = overrideVar || this.locals.hasOwnProperty(node.toString());
      // Check usage context to determin if variable is being read (versus written):
      var readingVar = (parent.type === NodeType.VALUE ||
                        parent.type === NodeType.INTERPOLATION ||
                        parent.type === NodeType.PARENTHESIS ||
                        parent.type === NodeType.ARGUMENTS ||
                        parent.type === NodeType.LOOP);

      if (this._template && overrideVar && readingVar) {
        if (parent.type === NodeType.ARGUMENTS) {
          throw 'Theme variables are not permitted as function arguments when rendering templates.';
        }

        var id = node.first(NodeType.IDENTIFIER);
        node.type = NodeType.IDENTIFIER;
        node.content = '____'+ id.content +'____';
      }

      // Keep if the variable is a property being written ($banana: yellow;),
      // or if the variable is keepable and being read (color: $banana;).
      return  (parent.type === NodeType.PROPERTY) || (readingVar && keepableVar);
    }

    // Keep "@include mixin-name;" statements for keepable mixin:
    else if (node.type === NodeType.EXTEND) {
      var extend = node.toString().replace(/@extend\s+(.+)$/, '$1').trim();
      if (this.extend.hasOwnProperty(extend)) return true;
    }

    // Keep "@include mixin-name;" statements for keepable mixin:
    else if (node.type === NodeType.INCLUDE) {
      var include = node.toString().replace(/@include\s+(.+)$/, '$1').trim();
      if (this.mixin.hasOwnProperty(include)) return true;
    }

    // Drop all vars stylesheet includes:
    // This removes the overrideable variables from the compilation entirely.
    else if (node.type === NodeType.STYLE_SHEET && node.filepath === this.varsFile) {
      return this.dropNode(node, 'varsfile');
    }

    // Extend filter to all child nodes...
    if (Array.isArray(node.content)) {
      // Recursively filter on all node children:
      for (var i=0; i < node.content.length; i++) {
        keep = this.reduceNode(node.content[i], node) || keep;
      }
    }

    // Track mixin names that contain keepable variables:
    if (node.type === NodeType.MIXIN && keep) {
      var ident = node.first(NodeType.IDENTIFIER);
      if (ident && typeof ident.content === 'string') {
        this.mixin[ ident.content ] = 1;
      }
    }

    // Track valid rulesets for future use by @extend:
    // Remove last selector after traversing a ruleset.
    else if (node.type === NodeType.RULESET) {

      // Retain all selector names that are being kept:
      if (keep && this.selector.length) {
        this.extend[ this.selector.join(' ') ] = 1;
      }

      this.selector.pop();
    }

    // Clear local variables after completing a loop:
    else if (node.type === NodeType.LOOP) {
      this.locals = {};
    }

    // If this is a removable node that we're NOT keeping, drop it:
    if (!keep && this.isRemovable(node.type)) {
      return this.dropNode(node);
    }

    return keep;
  },

  // Invokes pruning on the theme's AST:
  prune: function() {
    this._template = false;
    this.reduceNode(this.ast);
    return this;
  },

  // Invokes pruning, and then compiles to CSS:
  css: function(done) {
    this._template = false;
    this.reduceNode(this.ast);

    if (!this.themeData) {
      if (!this.themeFile) throw 'No theme variables file specified.';
      if (!path.isAbsolute(this.themeFile)) {
        this.themeFile = path.resolve(this.cwd, this.themeFile);
      }

      this.themeData = fs.readFileSync(this.themeFile, 'utf-8');
    }

    // Compile:
    require('node-sass').render({
        outputStyle: this.outputStyle,
        data: this.themeData +'\n'+ this.ast.toString(),
        success: complete // v2
      }, complete); // v3

    // Callback for Sass compilation:
    // compatible with node-sass v2 & v3.
    function complete(err, result) {
      if (err && typeof err.css === 'string') result = err; // v2
      else if (err) throw err; // v3
      done(err, result.css.toString());
    }

    return this;
  },

  // Renders a flat CSS template with interpolation fields:
  template: function(done) {
    var self = this;
    this._template = true;
    this.reduceNode(this.ast);

    // Compile:
    require('node-sass').render({
        outputStyle: this.outputStyle,
        data: this.varsText +'\n'+ this.ast.toString(),
        success: complete // v2
      }, complete); // v3

    // Callback for Sass compilation:
    // compatible with node-sass v2 & v3.
    function complete(err, result) {
      if (err && typeof err.css === 'string') result = err; // v2
      else if (err) throw err; // v3

      // Replace variable identifiers with interpolation fields:
      var css = result.css.toString().replace(/____(.+?)____/g, function(match, $1) {
        var field = self.templateSnakeCase ? $1.toLowerCase().replace(/-/g, '_') : $1;
        return [self.templateOpen, ' ', field, ' ', self.templateClose].join('');
      });

      done(err, css);
    }

    return this;
  }
};

module.exports = Thematic;<commit_msg>Improving template error message.<commit_after>var fs = require('fs');
var path = require('path');
var gonzales = require('gonzales-pe');

var NodeType = {
  ATRULE_RQ: 'atrulerq',
  ARGUMENTS: 'arguments',
  BLOCK: 'block',
  COMMENT: 'singlelineComment',
  DECLARATION: 'declaration',
  DELIMITER_D: 'declarationDelimiter',
  DELIMITER_P: 'propertyDelimiter',
  EXTEND: 'extend',
  FUNCTION: 'function',
  IDENTIFIER: 'ident',
  INCLUDE: 'include',
  INTERPOLATION: 'interpolation',
  LOOP: 'loop',
  MIXIN: 'mixin',
  PARENTHESIS: 'parentheses',
  PROPERTY: 'property',
  RULESET: 'ruleset',
  SELECTOR: 'selector',
  SIMPLE_SELECTOR: 'simpleSelector',
  STYLE_SHEET: 'stylesheet',
  VALUE: 'value',
  VARIABLE: 'variable'
};

var reducerOptions = ['varsFile', 'themeFile', 'themeData', 'templateOpen', 'templateClose', 'templateSnakeCase', 'outputStyle'];

function Thematic(ast, opts) {
  this.ast = ast;
  this.cwd = opts.cwd || process.cwd();
  this.selector = [];
  this.vars = {};
  this.locals = {};
  this.extend = {};
  this.mixin = {};

  // Extend valid option names onto this object:
  for (var i=0; i < reducerOptions.length; i++) {
    var attr = reducerOptions[i];
    if (opts.hasOwnProperty(attr)) {
      this[attr] = opts[attr];
    }
  }

  // Resolve variables file path:
  if (!this.varsFile) throw 'No variables file specified.';
  if (!path.isAbsolute(this.varsFile)) {
    this.varsFile = path.resolve(this.cwd, this.varsFile);
  }

  // Write all theme variables into the reducer vars cache:
  this.varsText = fs.readFileSync(this.varsFile, 'utf-8');
  var pattern = /(\$[^\s:]+)[\s:]/g;
  var match = pattern.exec(this.varsText);

  while (match) {
    this.vars[ match[1] ] = 1;
    match = pattern.exec(this.varsText);
  }
}

Thematic.prototype = {
  varsFile: null,
  _template: false,
  templateOpen: '<%=',
  templateClose: '%>',
  outputStyle: 'nested',

  // List of removable node types:
  removeableTypes: [
    NodeType.DECLARATION,
    NodeType.EXTEND,
    NodeType.INCLUDE,
    NodeType.LOOP,
    NodeType.MIXIN,
    NodeType.RULESET
  ],

  // Checks if a type is removable:
  // constructs and caches types hash on first lookup.
  isRemovable: function(type) {
    return this.removeableTypes.indexOf(type) != -1;
  },

  /**
  * Drops a node from the tree:
  * the node's content is replaced by a single-line comment.
  */
  dropNode: function(node, desc) {
    node.content = ' ' + (desc || node.type);
    node.type = NodeType.COMMENT;
    return false;
  },

  /**
  * Collects local variable definitions that may
  * derrive from an override variable:
  * Ex: `@each $c in $keep-color`
  */
  setLocals: function(node) {
    var locals = {};
    var keep = false;
    var loop = node.toString().split('\n')[0];
    var pattern = /(\$[\w-]+)/g;
    var match = pattern.exec(loop);

    while (match) {
      locals[ match[1] ] = 1;
      keep = this.vars.hasOwnProperty(match[1]) || keep;
      match = pattern.exec(loop);
    }

    // Only keep these locals if there was a valid override within the loop:
    this.locals = keep ? locals : {};
  },

  /**
   * Methodology for recursively filtering a Sass abstract syntax tree (AST).
   * Each node is explored for relevant variables, and then pruned if irrelevant.
   * @param { object } node: a Gonzales Node object from a parsed AST.
   * @param { array } ancestry: an array of node-type strings prepresenting the current search path.
   * @return { boolean } returns true if the search branch should be retained.
   */
  reduceNode: function(node, parent) {
    var keep = false;

    // Track selector heirarchy while drilling into the tree:
    if (node.type === NodeType.SELECTOR) {
      this.selector.push(node.toString().trim());
    }

    // Track derivitive locals created by control-flow loops:
    else if (node.type === NodeType.LOOP) {
      this.setLocals(node);
    }

    // Keep variable declarations and overrides:
    else if (node.type === NodeType.VARIABLE && parent) {
      // Test if the var belongs to the set of override vars:
      var overrideVar = this.vars.hasOwnProperty(node.toString());
      // Test if the variable is keepable (includes local derivatives)
      var keepableVar = overrideVar || this.locals.hasOwnProperty(node.toString());
      // Check usage context to determin if variable is being read (versus written):
      var readingVar = (parent.type === NodeType.VALUE ||
                        parent.type === NodeType.INTERPOLATION ||
                        parent.type === NodeType.PARENTHESIS ||
                        parent.type === NodeType.ARGUMENTS ||
                        parent.type === NodeType.LOOP);

      if (this._template && overrideVar && readingVar) {
        if (parent.type === NodeType.ARGUMENTS) {
          throw 'Theme variables are not permitted as arguments when rendering templates:\n--> '+parent.toString();
        }

        var id = node.first(NodeType.IDENTIFIER);
        node.type = NodeType.IDENTIFIER;
        node.content = '____'+ id.content +'____';
      }

      // Keep if the variable is a property being written ($banana: yellow;),
      // or if the variable is keepable and being read (color: $banana;).
      return  (parent.type === NodeType.PROPERTY) || (readingVar && keepableVar);
    }

    // Keep "@include mixin-name;" statements for keepable mixin:
    else if (node.type === NodeType.EXTEND) {
      var extend = node.toString().replace(/@extend\s+(.+)$/, '$1').trim();
      if (this.extend.hasOwnProperty(extend)) return true;
    }

    // Keep "@include mixin-name;" statements for keepable mixin:
    else if (node.type === NodeType.INCLUDE) {
      var include = node.toString().replace(/@include\s+(.+)$/, '$1').trim();
      if (this.mixin.hasOwnProperty(include)) return true;
    }

    // Drop all vars stylesheet includes:
    // This removes the overrideable variables from the compilation entirely.
    else if (node.type === NodeType.STYLE_SHEET && node.filepath === this.varsFile) {
      return this.dropNode(node, 'varsfile');
    }

    // Extend filter to all child nodes...
    if (Array.isArray(node.content)) {
      // Recursively filter on all node children:
      for (var i=0; i < node.content.length; i++) {
        keep = this.reduceNode(node.content[i], node) || keep;
      }
    }

    // Track mixin names that contain keepable variables:
    if (node.type === NodeType.MIXIN && keep) {
      var ident = node.first(NodeType.IDENTIFIER);
      if (ident && typeof ident.content === 'string') {
        this.mixin[ ident.content ] = 1;
      }
    }

    // Track valid rulesets for future use by @extend:
    // Remove last selector after traversing a ruleset.
    else if (node.type === NodeType.RULESET) {

      // Retain all selector names that are being kept:
      if (keep && this.selector.length) {
        this.extend[ this.selector.join(' ') ] = 1;
      }

      this.selector.pop();
    }

    // Clear local variables after completing a loop:
    else if (node.type === NodeType.LOOP) {
      this.locals = {};
    }

    // If this is a removable node that we're NOT keeping, drop it:
    if (!keep && this.isRemovable(node.type)) {
      return this.dropNode(node);
    }

    return keep;
  },

  // Invokes pruning on the theme's AST:
  prune: function() {
    this._template = false;
    this.reduceNode(this.ast);
    return this;
  },

  // Invokes pruning, and then compiles to CSS:
  css: function(done) {
    this._template = false;
    this.reduceNode(this.ast);

    if (!this.themeData) {
      if (!this.themeFile) throw 'No theme variables file specified.';
      if (!path.isAbsolute(this.themeFile)) {
        this.themeFile = path.resolve(this.cwd, this.themeFile);
      }

      this.themeData = fs.readFileSync(this.themeFile, 'utf-8');
    }

    // Compile:
    require('node-sass').render({
        outputStyle: this.outputStyle,
        data: this.themeData +'\n'+ this.ast.toString(),
        success: complete // v2
      }, complete); // v3

    // Callback for Sass compilation:
    // compatible with node-sass v2 & v3.
    function complete(err, result) {
      if (err && typeof err.css === 'string') result = err; // v2
      else if (err) throw err; // v3
      done(err, result.css.toString());
    }

    return this;
  },

  // Renders a flat CSS template with interpolation fields:
  template: function(done) {
    var self = this;
    this._template = true;
    this.reduceNode(this.ast);

    // Compile:
    require('node-sass').render({
        outputStyle: this.outputStyle,
        data: this.varsText +'\n'+ this.ast.toString(),
        success: complete // v2
      }, complete); // v3

    // Callback for Sass compilation:
    // compatible with node-sass v2 & v3.
    function complete(err, result) {
      if (err && typeof err.css === 'string') result = err; // v2
      else if (err) throw err; // v3

      // Replace variable identifiers with interpolation fields:
      var css = result.css.toString().replace(/____(.+?)____/g, function(match, $1) {
        var field = self.templateSnakeCase ? $1.toLowerCase().replace(/-/g, '_') : $1;
        return [self.templateOpen, ' ', field, ' ', self.templateClose].join('');
      });

      done(err, css);
    }

    return this;
  }
};

module.exports = Thematic;<|endoftext|>


<commit_before>/*jshint browser:true */
/*globals Base64Binary */
(function($) {
	"use strict";

	var XmlRpcFault = function() {
		Error.apply(this, arguments);
	};
	XmlRpcFault.prototype = new Error();
	XmlRpcFault.prototype.type = 'XML-RPC fault';

	var xmlrpc = $.xmlrpc = function(url, settings) {

		if (arguments.length == 2) {
			settings.url = url;
		} else {
			settings = url;
			url = settings.url;
		}

		settings.dataType = 'xml json';
		settings.type = 'POST';
		settings.contentType = 'text/xml';
		settings.converters = {'xml json': xmlrpc.parseDocument};

		var xmlDoc = xmlrpc.document(settings.methodName, settings.params);
		var serializer = new XMLSerializer();

		settings.data = serializer.serializeToString(xmlDoc);

		return $.ajax(settings);
	};

	/**
	* Make an XML-RPC document from a method name and a set of parameters
	*/
	xmlrpc.document = function(name, params) {
		var doc = document.implementation.createDocument(null, null, null);
		var $xml = function(name) {
			return $(doc.createElement(name));
		};

		var $methodName = $xml('methodName').text(name);
		var $params = $xml('params').append($.map(params, function(param) {
			var $value = $xml('value').append(xmlrpc.toXmlRpc(param, $xml));
			return $xml('param').append($value);
		}));
		var $methodCall = $xml('methodCall').append($methodName, $params);
		doc.appendChild($methodCall.get(0));
		return doc;
	};

	var _isInt = function(x) {
		return (x === parseInt(x, 10)) && !isNaN(x);
	};

	/**
	* Take a JavaScript value, and return an XML node representing the value
	* in XML-RPC style. If the value is one of the `XmlRpcType`s, that type is
	* used. Otherwise, a best guess is made as to its type. The best guess is
	* good enough in the vast majority of cases.
	*/
	xmlrpc.toXmlRpc = function(item, $xml) {

		if (item instanceof XmlRpcType) return item.toXmlRpc($xml);

		var types = $.xmlrpc.types;
		var type = $.type(item);

		switch (type) {
			case "undefined":
			case "null":
				return types.nil.encode(item, $xml);

			case "date":
				return types['date.iso8601'].encode(item, $xml);

			case "object":
				if (item instanceof ArrayBuffer) {
					return types.base64.encode(item, $xml);
				} else {
					return types.struct.encode(item, $xml);
				}
				break;

			case "number":
				// Ints and Floats encode differently
				if (_isInt(item)) {
					return types.int.encode(item, $xml);
				} else {
					return types.double.encode(item, $xml);
				}
				break;

			case "array":
			case "boolean":
			case "string":
				return types[type].encode(item, $xml);

			default:
				throw new Error("Unknown type", item);
		}
	};

	/**
	* Take an XML-RPC document and decode it to an equivalent JavaScript
	* representation.
	*
	* If the XML-RPC document represents a fault, then an equivalent
	* XmlRpcFault will be thrown instead
	*/
	xmlrpc.parseDocument = function(doc) {
		var $doc = $(doc);
		var $response = $doc.children('methodresponse');
		var data;

		var $fault = $response.find('> fault');
		if ($fault.length === 0) {
			var $params = $response.find('> params > param > value > *');
			var json = $params.toArray().map(xmlrpc.parseNode);
			data = {
				error: false,
				data: json
			};
		} else {
			var fault = xmlrpc.parseNode($fault.find('> value > *').get(0));
			var err = new XmlRpcFault(fault.faultString);
			err.msg = err.message = fault.faultString;
			err.type = err.code = fault.faultCode;
			throw err;
		}

		return data;
	};

	/*
	* Take an XML-RPC node, and return the JavaScript equivalent
	*/
	xmlrpc.parseNode = function(node) {
		var nodename = node.nodeName.toLowerCase();
		if (nodename in xmlrpc.types) {
			return xmlrpc.types[nodename].decode(node);
		} else {
			throw new Error('Unknown type ' + nodename);
		}
	};

	var XmlRpcType = function() { };

	$.xmlrpc.types = {};

	/**
	* Make a XML-RPC type. We use these to encode and decode values. You can
	* also force a values type using this. See `$.xmlrpc.force()`
	*/
	xmlrpc.makeType = function(tagName, simple, encode, decode) {
		var Type;

		Type = function(value) {
			this.value = value;
		};
		Type.prototype = new XmlRpcType();
		Type.prototype.tagName = tagName;

		if (simple) {
			var simpleEncode = encode, simpleDecode = decode;
			encode = function(value, $xml) {
				var text = simpleEncode(value);
				return $xml(Type.tagName).text(text);
			};
			decode = function(node) {
				return simpleDecode($(node).text(), node);
			};
		}
		Type.prototype.toXmlRpc = function($xml) {
			return Type.encode(this.value, $xml);
		};

		Type.tagName = tagName;
		Type.encode = encode;
		Type.decode = decode;

		xmlrpc.types[tagName] = Type;
	};

	// Number types
	var _fromInt = function(value) { return '' + Math.floor(value); };
	var _toInt = function(text, node) { return parseInt(text, 10); };

	xmlrpc.makeType('int', true, _fromInt, _toInt),
	xmlrpc.makeType('i4', true, _fromInt, _toInt),
	xmlrpc.makeType('i8', true, _fromInt, _toInt),
	xmlrpc.makeType('i16', true, _fromInt, _toInt),
	xmlrpc.makeType('i32', true, _fromInt, _toInt),

	xmlrpc.makeType('double', true, String, function(text) {
		return parseFloat(text, 10);
	});

	// String type. Fairly simple
	xmlrpc.makeType('string', true, String, String);

	// Boolean type. True == '1', False == '0'
	xmlrpc.makeType('boolean', true, function(value) {
		return value ? '1' : '0';
	}, function(text) {
		return text == '1';
	});

	// Dates are a little trickier
	var _pad = function(n) { return n<10 ? '0'+n : n; };

	xmlrpc.makeType('date.iso8601', true, function(d) {
		return [
			d.getUTCFullYear(), '-', _pad(d.getUTCMonth()+1), '-',
			_pad(d.getUTCDate()), 'T', _pad(d.getUTCHours()), ':',
			_pad(d.getUTCMinutes()), ':', _pad(d.getUTCSeconds()), 'Z'
		].join('');
	}, function(text) {
		return new Date(text);
	});

	// Go between a base64 string and an ArrayBuffer
	xmlrpc.binary = (function() {
		var pad = '=';
		var toChars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
			'abcdefghijklmnopqrstuvwxyz0123456789+/').split("");
		var fromChars = toChars.reduce(function(acc, chr, i) {
			acc[chr] = i;
			return acc;
		}, {});

		/*
		* In the following, three bytes are added together into a 24-bit
		* number, which is then split up in to 4 6-bit numbers - or vice versa.
		* That is why there is lots of shifting by multiples of 6 and 8, and
		* the magic numbers 3 and 4.
		*
		* The modulo 64 is for converting to base 64, and the modulo 256 is for
		* converting to 8-bit numbers.
		*/
		return {
			toBase64: function(ab) {
				var acc = [];

				var int8View = new Uint8Array(ab);
				var int8Index = 0, int24;
				for (; int8Index < int8View.length; int8Index += 3) {

					// Grab three bytes
					int24 =
						(int8View[int8Index + 0] << 16) +
						(int8View[int8Index + 1] << 8) +
						(int8View[int8Index + 2] << 0);

					// Push four chars
					acc.push(toChars[(int24 >> 18) % 64]);
					acc.push(toChars[(int24 >> 12) % 64]);
					acc.push(toChars[(int24 >> 6) % 64]);
					acc.push(toChars[(int24 >> 0)% 64]);
				}

				// Set the last few characters to the padding character
				var padChars = 3 - ((ab.byteLength % 3) || 3);
				while (padChars--) {
					acc[acc.length - padChars - 1] = pad;
				}

				return acc.join('');
			},

			fromBase64: function(base64) {
				var base64Len = base64.length;

				// Work out the length of the data, accommodating for padding
				var abLen = (base64Len / 4) * 3;
				if (base64.charAt(base64Len - 1) == pad) abLen--;
				if (base64.charAt(base64Len - 2) == pad) abLen--;

				// Make the ArrayBuffer, and an Int8Array to work with it
				var ab = new ArrayBuffer(abLen);
				var int8View = new Uint8Array(ab);

				var base64Index = 0, int8Index = 0, int24;
				for (; base64Index < base64Len; base64Index += 4, int8Index += 3) {

					// Grab four chars
					int24 =
						(fromChars[base64[base64Index + 0]] << 18) +
						(fromChars[base64[base64Index + 1]] << 12) +
						(fromChars[base64[base64Index + 2]] << 6) +
						(fromChars[base64[base64Index + 3]] << 0);

					// Push three bytes
					int8View[int8Index + 0] = (int24 >> 16) % 256;
					int8View[int8Index + 1] = (int24 >> 8) % 256;
					int8View[int8Index + 2] = (int24 >> 0) % 256;

				}

				return ab;
			}
		};
	})();

	xmlrpc.makeType('base64', true, function(ab) {
		return xmlrpc.binary.toBase64(ab);
	}, function(text) {
		return xmlrpc.binary.fromBase64(text);
	});

	// Nil/null
	xmlrpc.makeType('nil', false,
		function(val, $xml) { return $xml('nil'); },
		function(node) { return null; }
	);

	// Structs/Objects
	xmlrpc.makeType('struct', false, function(value, $xml) {
		var $struct = $xml('struct');

		$.each(value, function(name, value) {
			var $name = $xml('name').text(name);
			var $value = $xml('value').append(xmlrpc.toXmlRpc(value, $xml));
			$struct.append($xml('member').append($name, $value));
		});

		return $struct;

	}, function(node) {
		return $(node)
			.find('> member')
			.toArray()
			.reduce(function(struct, el) {
				var $el = $(el);
				var key = $el.find('> name').text();
				var value = xmlrpc.parseNode($el.find('> value > *').get(0));
				struct[key] = value;
				return struct;
			}, {});

	});

	// Arrays
	xmlrpc.makeType('array', false, function(value, $xml) {
		var $array = $xml('array');
		var $data = $xml('data');
		$.each(value, function(i, val) {
			$data.append($xml('value').append(xmlrpc.toXmlRpc(val, $xml)));
		});
		$array.append($data);
		return $array;
	}, function(node) {
		var $values = $(node).find('> data > value > *');
		return $.map($values, xmlrpc.parseNode);
	});

	/**
	* Force a value to an XML-RPC type. All the usual XML-RPC types are
	* supported
	*/
	xmlrpc.force = function(type, value) {
		return new xmlrpc.types[type](value);
	};

})(jQuery);
<commit_msg>Fix nested array decoding<commit_after>/*jshint browser:true */
/*globals Base64Binary */
(function($) {
	"use strict";

	var XmlRpcFault = function() {
		Error.apply(this, arguments);
	};
	XmlRpcFault.prototype = new Error();
	XmlRpcFault.prototype.type = 'XML-RPC fault';

	var xmlrpc = $.xmlrpc = function(url, settings) {

		if (arguments.length == 2) {
			settings.url = url;
		} else {
			settings = url;
			url = settings.url;
		}

		settings.dataType = 'xml json';
		settings.type = 'POST';
		settings.contentType = 'text/xml';
		settings.converters = {'xml json': xmlrpc.parseDocument};

		var xmlDoc = xmlrpc.document(settings.methodName, settings.params);
		var serializer = new XMLSerializer();

		settings.data = serializer.serializeToString(xmlDoc);

		return $.ajax(settings);
	};

	/**
	* Make an XML-RPC document from a method name and a set of parameters
	*/
	xmlrpc.document = function(name, params) {
		var doc = document.implementation.createDocument(null, null, null);
		var $xml = function(name) {
			return $(doc.createElement(name));
		};

		var $methodName = $xml('methodName').text(name);
		var $params = $xml('params').append($.map(params, function(param) {
			var $value = $xml('value').append(xmlrpc.toXmlRpc(param, $xml));
			return $xml('param').append($value);
		}));
		var $methodCall = $xml('methodCall').append($methodName, $params);
		doc.appendChild($methodCall.get(0));
		return doc;
	};

	var _isInt = function(x) {
		return (x === parseInt(x, 10)) && !isNaN(x);
	};

	/**
	* Take a JavaScript value, and return an XML node representing the value
	* in XML-RPC style. If the value is one of the `XmlRpcType`s, that type is
	* used. Otherwise, a best guess is made as to its type. The best guess is
	* good enough in the vast majority of cases.
	*/
	xmlrpc.toXmlRpc = function(item, $xml) {

		if (item instanceof XmlRpcType) return item.toXmlRpc($xml);

		var types = $.xmlrpc.types;
		var type = $.type(item);

		switch (type) {
			case "undefined":
			case "null":
				return types.nil.encode(item, $xml);

			case "date":
				return types['date.iso8601'].encode(item, $xml);

			case "object":
				if (item instanceof ArrayBuffer) {
					return types.base64.encode(item, $xml);
				} else {
					return types.struct.encode(item, $xml);
				}
				break;

			case "number":
				// Ints and Floats encode differently
				if (_isInt(item)) {
					return types.int.encode(item, $xml);
				} else {
					return types.double.encode(item, $xml);
				}
				break;

			case "array":
			case "boolean":
			case "string":
				return types[type].encode(item, $xml);

			default:
				throw new Error("Unknown type", item);
		}
	};

	/**
	* Take an XML-RPC document and decode it to an equivalent JavaScript
	* representation.
	*
	* If the XML-RPC document represents a fault, then an equivalent
	* XmlRpcFault will be thrown instead
	*/
	xmlrpc.parseDocument = function(doc) {
		var $doc = $(doc);
		var $response = $doc.children('methodresponse');
		var data;

		var $fault = $response.find('> fault');
		if ($fault.length === 0) {
			var $params = $response.find('> params > param > value > *');
			var json = $params.toArray().map(xmlrpc.parseNode);
			data = {
				error: false,
				data: json
			};
		} else {
			var fault = xmlrpc.parseNode($fault.find('> value > *').get(0));
			var err = new XmlRpcFault(fault.faultString);
			err.msg = err.message = fault.faultString;
			err.type = err.code = fault.faultCode;
			throw err;
		}

		return data;
	};

	/*
	* Take an XML-RPC node, and return the JavaScript equivalent
	*/
	xmlrpc.parseNode = function(node) {
		var nodename = node.nodeName.toLowerCase();
		if (nodename in xmlrpc.types) {
			return xmlrpc.types[nodename].decode(node);
		} else {
			throw new Error('Unknown type ' + nodename);
		}
	};

	var XmlRpcType = function() { };

	$.xmlrpc.types = {};

	/**
	* Make a XML-RPC type. We use these to encode and decode values. You can
	* also force a values type using this. See `$.xmlrpc.force()`
	*/
	xmlrpc.makeType = function(tagName, simple, encode, decode) {
		var Type;

		Type = function(value) {
			this.value = value;
		};
		Type.prototype = new XmlRpcType();
		Type.prototype.tagName = tagName;

		if (simple) {
			var simpleEncode = encode, simpleDecode = decode;
			encode = function(value, $xml) {
				var text = simpleEncode(value);
				return $xml(Type.tagName).text(text);
			};
			decode = function(node) {
				return simpleDecode($(node).text(), node);
			};
		}
		Type.prototype.toXmlRpc = function($xml) {
			return Type.encode(this.value, $xml);
		};

		Type.tagName = tagName;
		Type.encode = encode;
		Type.decode = decode;

		xmlrpc.types[tagName] = Type;
	};

	// Number types
	var _fromInt = function(value) { return '' + Math.floor(value); };
	var _toInt = function(text, node) { return parseInt(text, 10); };

	xmlrpc.makeType('int', true, _fromInt, _toInt),
	xmlrpc.makeType('i4', true, _fromInt, _toInt),
	xmlrpc.makeType('i8', true, _fromInt, _toInt),
	xmlrpc.makeType('i16', true, _fromInt, _toInt),
	xmlrpc.makeType('i32', true, _fromInt, _toInt),

	xmlrpc.makeType('double', true, String, function(text) {
		return parseFloat(text, 10);
	});

	// String type. Fairly simple
	xmlrpc.makeType('string', true, String, String);

	// Boolean type. True == '1', False == '0'
	xmlrpc.makeType('boolean', true, function(value) {
		return value ? '1' : '0';
	}, function(text) {
		return text == '1';
	});

	// Dates are a little trickier
	var _pad = function(n) { return n<10 ? '0'+n : n; };

	xmlrpc.makeType('date.iso8601', true, function(d) {
		return [
			d.getUTCFullYear(), '-', _pad(d.getUTCMonth()+1), '-',
			_pad(d.getUTCDate()), 'T', _pad(d.getUTCHours()), ':',
			_pad(d.getUTCMinutes()), ':', _pad(d.getUTCSeconds()), 'Z'
		].join('');
	}, function(text) {
		return new Date(text);
	});

	// Go between a base64 string and an ArrayBuffer
	xmlrpc.binary = (function() {
		var pad = '=';
		var toChars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
			'abcdefghijklmnopqrstuvwxyz0123456789+/').split("");
		var fromChars = toChars.reduce(function(acc, chr, i) {
			acc[chr] = i;
			return acc;
		}, {});

		/*
		* In the following, three bytes are added together into a 24-bit
		* number, which is then split up in to 4 6-bit numbers - or vice versa.
		* That is why there is lots of shifting by multiples of 6 and 8, and
		* the magic numbers 3 and 4.
		*
		* The modulo 64 is for converting to base 64, and the modulo 256 is for
		* converting to 8-bit numbers.
		*/
		return {
			toBase64: function(ab) {
				var acc = [];

				var int8View = new Uint8Array(ab);
				var int8Index = 0, int24;
				for (; int8Index < int8View.length; int8Index += 3) {

					// Grab three bytes
					int24 =
						(int8View[int8Index + 0] << 16) +
						(int8View[int8Index + 1] << 8) +
						(int8View[int8Index + 2] << 0);

					// Push four chars
					acc.push(toChars[(int24 >> 18) % 64]);
					acc.push(toChars[(int24 >> 12) % 64]);
					acc.push(toChars[(int24 >> 6) % 64]);
					acc.push(toChars[(int24 >> 0)% 64]);
				}

				// Set the last few characters to the padding character
				var padChars = 3 - ((ab.byteLength % 3) || 3);
				while (padChars--) {
					acc[acc.length - padChars - 1] = pad;
				}

				return acc.join('');
			},

			fromBase64: function(base64) {
				var base64Len = base64.length;

				// Work out the length of the data, accommodating for padding
				var abLen = (base64Len / 4) * 3;
				if (base64.charAt(base64Len - 1) == pad) abLen--;
				if (base64.charAt(base64Len - 2) == pad) abLen--;

				// Make the ArrayBuffer, and an Int8Array to work with it
				var ab = new ArrayBuffer(abLen);
				var int8View = new Uint8Array(ab);

				var base64Index = 0, int8Index = 0, int24;
				for (; base64Index < base64Len; base64Index += 4, int8Index += 3) {

					// Grab four chars
					int24 =
						(fromChars[base64[base64Index + 0]] << 18) +
						(fromChars[base64[base64Index + 1]] << 12) +
						(fromChars[base64[base64Index + 2]] << 6) +
						(fromChars[base64[base64Index + 3]] << 0);

					// Push three bytes
					int8View[int8Index + 0] = (int24 >> 16) % 256;
					int8View[int8Index + 1] = (int24 >> 8) % 256;
					int8View[int8Index + 2] = (int24 >> 0) % 256;

				}

				return ab;
			}
		};
	})();

	xmlrpc.makeType('base64', true, function(ab) {
		return xmlrpc.binary.toBase64(ab);
	}, function(text) {
		return xmlrpc.binary.fromBase64(text);
	});

	// Nil/null
	xmlrpc.makeType('nil', false,
		function(val, $xml) { return $xml('nil'); },
		function(node) { return null; }
	);

	// Structs/Objects
	xmlrpc.makeType('struct', false, function(value, $xml) {
		var $struct = $xml('struct');

		$.each(value, function(name, value) {
			var $name = $xml('name').text(name);
			var $value = $xml('value').append(xmlrpc.toXmlRpc(value, $xml));
			$struct.append($xml('member').append($name, $value));
		});

		return $struct;

	}, function(node) {
		return $(node)
			.find('> member')
			.toArray()
			.reduce(function(struct, el) {
				var $el = $(el);
				var key = $el.find('> name').text();
				var value = xmlrpc.parseNode($el.find('> value > *').get(0));
				struct[key] = value;
				return struct;
			}, {});

	});

	// Arrays
	xmlrpc.makeType('array', false, function(value, $xml) {
		var $array = $xml('array');
		var $data = $xml('data');
		$.each(value, function(i, val) {
			$data.append($xml('value').append(xmlrpc.toXmlRpc(val, $xml)));
		});
		$array.append($data);
		return $array;
	}, function(node) {
		var $values = $(node).find('> data > value > *');
		var decoded = $values.toArray().map(xmlrpc.parseNode);
		return decoded;
	});

	/**
	* Force a value to an XML-RPC type. All the usual XML-RPC types are
	* supported
	*/
	xmlrpc.force = function(type, value) {
		return new xmlrpc.types[type](value);
	};

})(jQuery);
<|endoftext|>


<commit_before>"use strict"

/**
 * Class for dealing with the connection to the game server. This class uses
 * web sockets since raw sockets are not available within browsers, so for
 * connecting to the game server some sort of proxy must be used.
 * @param host Name of the (proxy) host to connect to
 * @param port Port number to connect on
 */
function Connection(host, port)
{
	/// Private copy of the host name
	this.host = host;
	/// Private copy of the port number
	this.port = port;
	/// The web socket used to communicate
	this.socket = new WebSocket("ws://" + host + ":" + port, "binary");

	var this_obj = this;
	this.socket.binaryType = "arraybuffer";
	this.socket.onopen = function() { this_obj.onopen(); };
	this.socket.onclose = function() { this_obj.onclose(); };
	this.socket.onerror = function(event) { this_obj.onerror(event); };
	this.socket.onmessage = function(msg)
	{
		this_obj.onmessage(new Uint8Array(msg.data));
	};

	/// Timestamp of the last time we sent a message to the server
	this.last_send_time = 0;

	/// Size of the buffer for unprocessed (incomplete) server messages
	this.unprocessed_data_size = 256;
	/// Size of the buffer that is in use
	this.unprocessed_data_used = 0;
	/// The unprocessed data buffer itself
	this.unprocessed_data = new Uint8Array(this.unprocessed_data_size);
}

/// Interval we are able to not talk to the server before it closes the connection
Connection.heart_beat_timeout = 20000; // milliseconds

/**
 * Check if we should send a heartbeat to the server, and if so, do it.
 * Afterwards, reschedule to check again at an appropriate time.
 */
Connection.prototype.checkHeartbeat = function()
{
	if (!this.socket)
		// socket closed, return to stop heartbeat cycle
		return;

	var now = Date.now();
	var this_obj = this;
	if (now > this.last_send_time + 0.9 * Connection.heart_beat_timeout)
	{
		this.send(Protocol.ServerCmd.HEART_BEAT);
		window.setTimeout(function() { this_obj.checkHeartbeat(); },
			Connection.heart_beat_timeout);
	}
	else
	{
		window.setTimeout(function() { this_obj.checkHeartbeat(); },
			this.last_send_time + Connection.heart_beat_timeout - now);
	}
};

/**
 * Handle a message from the server.
 * @param opcode The command code of the server message
 * @param data   The data associated with the command
 */
Connection.prototype.handleMessage = function(opcode, data)
{
	switch (opcode)
	{
		case Protocol.ClientCmd.RAW_TEXT:
			console.log("Raw text");
			break;
		case Protocol.ClientCmd.PING_REQUEST:
			console.log("ping request");
			break;
		default:
			console.log("got " + opcode + " message with " + data.length + " bytes of data");
	}
};

/// Handle the connection being closed
Connection.prototype.onclose = function()
{
console.log("close");
	this.socket = null;
};

/// Handle an error in the connection to the server
Connection.prototype.onerror = function(err)
{
console.log("error:");
	console.log(err);
};

/**
 * Handle incoming data from the server. This splits the data into separate
 * messages which are handles by handleMessage().
 * @param data The data bytes read from the server
 */
Connection.prototype.onmessage = function(data)
{
	var needed = this.unprocessed_data_used + data.length;
	if (needed > this.unprocessed_data_size)
	{
		var size = 2 * this.unprocessed_data_size;
		while (needed > size)
			size *= 2;
		var new_data = new Uint8Array(size);
		new_data.set(data);
		this.unprocessed_data = data;
		this.unprocessed_data_size = size;
	}

	this.unprocessed_data.set(data, this.unprocessed_data_used);
	this.unprocessed_data_used = needed;

	var off = 0;
	while (off + 3 <= this.unprocessed_data_used)
	{
		var data_size = (this.unprocessed_data[off+1] | (this.unprocessed_data[off+2] << 8)) - 1;
		if (off + 3 + data_size > this.unprocessed_data_used)
			break;

		this.handleMessage(this.unprocessed_data[off],
			this.unprocessed_data.subarray(off+3, off+3+data_size));
		off += 3 + data_size;
	}

	this.unprocessed_data.copyWithin(0, off, this.unprocessed_data_used);
	this.unprocessed_data_used -= off;
};

/// Handle the connection to the server being opened
Connection.prototype.onopen = function()
{
console.log("open");
	this.checkHeartbeat();
};

/**
 * Send a message to the server
 * @param opcode The command code of the message
 * @param data   Data bytes associated with the message
 */
Connection.prototype.send = function(opcode, data)
{
	var bytes;
	if (!data)
	{
		bytes = Uint8Array.from([opcode, 1, 0]);
	}
	else
	{
		var count = data.length;
		if (count >= 0xffff)
			return;

		bytes = new Uint8Array(3 + count);
		bytes[0] = opcode;
		bytes[1] = (count+1) & 0xff;
		bytes[2] = ((count+1) >> 8) & 0xff;
		bytes.set(data, 3);
	}

	this.socket.send(bytes.buffer);

	this.last_send_time = Date.now();
console.log("send " + opcode + " with " + count + " bytes of data");
};<commit_msg>handle ping request from server<commit_after>"use strict"

/**
 * Class for dealing with the connection to the game server. This class uses
 * web sockets since raw sockets are not available within browsers, so for
 * connecting to the game server some sort of proxy must be used.
 * @param host Name of the (proxy) host to connect to
 * @param port Port number to connect on
 */
function Connection(host, port)
{
	/// Private copy of the host name
	this.host = host;
	/// Private copy of the port number
	this.port = port;
	/// The web socket used to communicate
	this.socket = new WebSocket("ws://" + host + ":" + port, "binary");

	var this_obj = this;
	this.socket.binaryType = "arraybuffer";
	this.socket.onopen = function() { this_obj.onopen(); };
	this.socket.onclose = function() { this_obj.onclose(); };
	this.socket.onerror = function(event) { this_obj.onerror(event); };
	this.socket.onmessage = function(msg)
	{
		this_obj.onmessage(new Uint8Array(msg.data));
	};

	/// Timestamp of the last time we sent a message to the server
	this.last_send_time = 0;

	/// Size of the buffer for unprocessed (incomplete) server messages
	this.unprocessed_data_size = 256;
	/// Size of the buffer that is in use
	this.unprocessed_data_used = 0;
	/// The unprocessed data buffer itself
	this.unprocessed_data = new Uint8Array(this.unprocessed_data_size);
}

/// Interval we are able to not talk to the server before it closes the connection
Connection.heart_beat_timeout = 20000; // milliseconds

/**
 * Check if we should send a heartbeat to the server, and if so, do it.
 * Afterwards, reschedule to check again at an appropriate time.
 */
Connection.prototype.checkHeartbeat = function()
{
	if (!this.socket)
		// socket closed, return to stop heartbeat cycle
		return;

	var now = Date.now();
	var this_obj = this;
	if (now > this.last_send_time + 0.9 * Connection.heart_beat_timeout)
	{
		this.send(Protocol.ServerCmd.HEART_BEAT);
		window.setTimeout(function() { this_obj.checkHeartbeat(); },
			Connection.heart_beat_timeout);
	}
	else
	{
		window.setTimeout(function() { this_obj.checkHeartbeat(); },
			this.last_send_time + Connection.heart_beat_timeout - now);
	}
};

/**
 * Handle a message from the server.
 * @param opcode The command code of the server message
 * @param data   The data associated with the command
 */
Connection.prototype.handleMessage = function(opcode, data)
{
	switch (opcode)
	{
		case Protocol.ClientCmd.RAW_TEXT:
			console.log("Raw text");
			break;
		case Protocol.ClientCmd.PING_REQUEST:
			// XXX NOTE: other-life adds some status information.
			// EL just wants the packet back as is
			this.send(Protocol.ServerCmd.PING_RESPONSE, data);
			break;
		default:
			console.log("got " + opcode + " message with " + data.length + " bytes of data");
	}
};

/// Handle the connection being closed
Connection.prototype.onclose = function()
{
console.log("close");
	this.socket = null;
};

/// Handle an error in the connection to the server
Connection.prototype.onerror = function(err)
{
console.log("error:");
	console.log(err);
};

/**
 * Handle incoming data from the server. This splits the data into separate
 * messages which are handles by handleMessage().
 * @param data The data bytes read from the server
 */
Connection.prototype.onmessage = function(data)
{
	var needed = this.unprocessed_data_used + data.length;
	if (needed > this.unprocessed_data_size)
	{
		var size = 2 * this.unprocessed_data_size;
		while (needed > size)
			size *= 2;
		var new_data = new Uint8Array(size);
		new_data.set(data);
		this.unprocessed_data = data;
		this.unprocessed_data_size = size;
	}

	this.unprocessed_data.set(data, this.unprocessed_data_used);
	this.unprocessed_data_used = needed;

	var off = 0;
	while (off + 3 <= this.unprocessed_data_used)
	{
		var data_size = (this.unprocessed_data[off+1] | (this.unprocessed_data[off+2] << 8)) - 1;
		if (off + 3 + data_size > this.unprocessed_data_used)
			break;

		this.handleMessage(this.unprocessed_data[off],
			this.unprocessed_data.subarray(off+3, off+3+data_size));
		off += 3 + data_size;
	}

	this.unprocessed_data.copyWithin(0, off, this.unprocessed_data_used);
	this.unprocessed_data_used -= off;
};

/// Handle the connection to the server being opened
Connection.prototype.onopen = function()
{
console.log("open");
	this.checkHeartbeat();
};

/**
 * Send a message to the server
 * @param opcode The command code of the message
 * @param data   Data bytes associated with the message
 */
Connection.prototype.send = function(opcode, data)
{
	var bytes;
	if (!data)
	{
		bytes = Uint8Array.from([opcode, 1, 0]);
	}
	else
	{
		var count = data.length;
		if (count >= 0xffff)
			return;

		bytes = new Uint8Array(3 + count);
		bytes[0] = opcode;
		bytes[1] = (count+1) & 0xff;
		bytes[2] = ((count+1) >> 8) & 0xff;
		bytes.set(data, 3);
	}

	this.socket.send(bytes.buffer);

	this.last_send_time = Date.now();
console.log("send " + opcode + " with " + count + " bytes of data");
};<|endoftext|>


<commit_before>/**
 * Creates a new Request instance
 *
 * @param {Object} application A Vroom Application instance
 * @param {Object} req A Node.js HTTP request instance
 * @param {Object} res A Node.js HTTP response instance
 * @returns {Vroom.Request} A new Request instance
 */
exports.Request = function Request(application, req, res) {
  this.application = application;
  this.req = req;
  this.res = res;

  this.LOG = this.application.LOG;

  this.hasSentHeader = false;
  this.finished = false;

  this._status = 404;
  this._header = {};
};

exports.Request.prototype = {
  /**
   * Get the status that is going to be or has been sent to the client.
   *
   * @returns {Number} The status code
   */
  get status() {
    return this._status;
  },

  /**
   * Set the status code to send to the client.
   * Must be called prior to Request.sendHeader.
   *
   * @param {Number} val The status code to send to the client
   */
  set status(val) {
    var s = new Number(val);
    if (!isNaN(s))
      this._status = s;
  },

  /**
   * Add a HTTP header field.
   * Must be called prior to sendHeader.
   *
   * @param {String} key The HTTP header key
   * @param {String} value The HTTP header value
   */
  addHeader: function(key, value) {
    this._header[key] = value;
  },

  /**
   * Add multiple HTTP headers.
   * Must be call prior to Request.sendHeader.
   *
   * @param {Object} headers Hash of headers to set
   */
  addHeaders: function(headers) {
    for (var k in headers)
      if (headers.hasOwnProperty(k))
        this.addHeaders(k, headers[k]);
  },

  /**
   * Remove a previously set HTTP header by key.
   * Must be called prior to Request.sendHeader.
   *
   * @param {String} key The HTTP header key to remove
   */
  removeHeader: function(key) {
    delete this._header[key];
  },

  /**
   * Send the HTTP status code and headers to the client. After this
   * is called the status code and headers can no longer be changed.
   * This should be called prior to Request.write.
   */
  sendHeader: function() {
    this.res.sendHeader(this._status, this._header);
    this.hasSentHeader = true;
  },

  /**
   * Sends a string to the client. Can be called any number of times after
   * Request.sendHeader but before Request.finish
   *
   * @param {String...} ... Strings to send to the client
   */
  write: function() {
    if (!this.hasSentHeader) {
      this.LOG.debug("Auto sending headers because 'write' was called before 'sendHeader'.");
      this.sendHeader();
    }

    if (!this.finished)
      for (var i = 0; i < arguments.length; i++)
        this.res.sendBody(arguments[i]);
  },

  /**
   * Finishes the request with the client. Must be called for every request.
   */
  finish: function() {
    this.res.finish();
    this.finished = true;
  },

  /**
   * Returns the HTTP parameters and memoizes it. Use the getter
   * instead of this method.
   *
   * @returns {Object} The HTTP parameters
   */
  getParams: function() {
    return this.req.uri.params;
  },

  /**
   * Get the parsed query string for the request.
   *
   * @returns {Object} The HTTP parsed parameters
   */
  get params() {
    return this.getParams();
  },

  /**
   * Get the path of the request.
   *
   * @returns {String} The request's path
   */
  get path() {
    return this.req.uri.path || "/";
  },

  /**
   * Get the request method ('GET', 'POST', 'PUT', 'DELETE', etc).
   *
   * @returns {String} The request method
   */
  get method() {
    switch (this.req.method) {
    case "OPTIONS":
    case "HEAD":
      return "GET";
    default:
      return this.req.method;
    }
  },

  /**
   * Returns the HTTP protocol.
   *
   * @returns {String} The HTTP protocol
   */
  get protocol() {
    return (this.req.uri.protocol || "http");
  },

  /**
   * Returns the host of the request.
   *
   * @returns {String} The HTTP host
   */
  get host() {
    return (this.req.uri.host || this.server.hostname || "");
  }
};
<commit_msg>Added some more comments to Vroom.Request.<commit_after>/**
 * Creates a new Request instance
 *
 * @property {Vroom.Application} application
 * @property {Vroom.Config} config
 * @property {Vroom.Logger} LOG
 * @property {Boolean} hasSentHeaders True once sendHeader() is called
 * @property {Boolean} finished True if finish() has been called
 *
 * @param {Object} application A Vroom Application instance
 * @param {Object} req A Node.js HTTP request instance
 * @param {Object} res A Node.js HTTP response instance
 * @returns {Vroom.Request} A new Request instance
 */
exports.Request = function Request(application, req, res) {
  this.application = application;
  this.req = req;
  this.res = res;

  this.config = this.application.config;
  this.LOG = this.application.LOG;

  this.hasSentHeader = false;
  this.finished = false;

  this._status = 404;
  this._header = {};
};

exports.Request.prototype = {
  /**
   * Get the status that is going to be or has been sent to the client.
   *
   * @returns {Number} The status code
   */
  get status() {
    return this._status;
  },

  /**
   * Set the status code to send to the client.
   * Must be called prior to Request.sendHeader.
   *
   * @param {Number} val The status code to send to the client
   */
  set status(val) {
    var s = new Number(val);
    if (!isNaN(s))
      this._status = s;
  },

  /**
   * Add a HTTP header field.
   * Must be called prior to sendHeader.
   *
   * @param {String} key The HTTP header key
   * @param {String} value The HTTP header value
   */
  addHeader: function(key, value) {
    this._header[key] = value;
  },

  /**
   * Add multiple HTTP headers.
   * Must be call prior to Request.sendHeader.
   *
   * @param {Object} headers Hash of headers to set
   */
  addHeaders: function(headers) {
    for (var k in headers)
      if (headers.hasOwnProperty(k))
        this.addHeaders(k, headers[k]);
  },

  /**
   * Remove a previously set HTTP header by key.
   * Must be called prior to Request.sendHeader.
   *
   * @param {String} key The HTTP header key to remove
   */
  removeHeader: function(key) {
    delete this._header[key];
  },

  /**
   * Send the HTTP status code and headers to the client. After this
   * is called the status code and headers can no longer be changed.
   * This should be called prior to Request.write.
   */
  sendHeader: function() {
    this.res.sendHeader(this._status, this._header);
    this.hasSentHeader = true;
  },

  /**
   * Sends a string to the client. Can be called any number of times after
   * Request.sendHeader but before Request.finish
   *
   * @param {String...} ... Strings to send to the client
   */
  write: function() {
    if (!this.hasSentHeader) {
      this.LOG.debug("Auto sending headers because 'write' was called before 'sendHeader'.");
      this.sendHeader();
    }

    if (!this.finished)
      for (var i = 0; i < arguments.length; i++)
        this.res.sendBody(arguments[i]);
  },

  /**
   * Finishes the request with the client. Must be called for every request.
   */
  finish: function() {
    this.res.finish();
    this.finished = true;
  },

  /**
   * Returns the HTTP parameters and memoizes it. Use the getter
   * instead of this method.
   *
   * @returns {Object} The HTTP parameters
   */
  getParams: function() {
    return this.req.uri.params;
  },

  /**
   * Get the parsed query string for the request.
   *
   * @returns {Object} The HTTP parsed parameters
   */
  get params() {
    return this.getParams();
  },

  /**
   * Get the path of the request.
   *
   * @returns {String} The request's path
   */
  get path() {
    return this.req.uri.path || "/";
  },

  /**
   * Get the request method ('GET', 'POST', 'PUT', 'DELETE', etc).
   *
   * @returns {String} The request method
   */
  get method() {
    switch (this.req.method) {
    case "OPTIONS":
    case "HEAD":
      return "GET";
    default:
      return this.req.method;
    }
  },

  /**
   * Returns the HTTP protocol.
   *
   * @returns {String} The HTTP protocol
   */
  get protocol() {
    return (this.req.uri.protocol || "http");
  },

  /**
   * Returns the host of the request.
   *
   * @returns {String} The HTTP host
   */
  get host() {
    return (this.req.uri.host || this.server.hostname || "");
  }
};
<|endoftext|>


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

/**
 * Faucet with a pinball machine 'shooter'.
 * Pulling out the shooter changes the flow rate.
 * Releasing the shooter sets the flow rate to zero.
 * When the faucet is disabled, the flow rate is set to zero and the shooter is disabled.
 * Origin is at the bottom-center of the spout.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // imports
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Timer = require( 'JOIST/Timer' );

  // images
  var knobImage = require( 'image!SCENERY_PHET/faucet_knob.png' );
  var knobDisabledImage = require( 'image!SCENERY_PHET/faucet_knob_disabled.png' );
  var shaftImage = require( 'image!SCENERY_PHET/faucet_shaft.png' );
  var flangeImage = require( 'image!SCENERY_PHET/faucet_flange.png' );
  var flangeDisabledImage = require( 'image!SCENERY_PHET/faucet_flange_disabled.png' );
  var stopImage = require( 'image!SCENERY_PHET/faucet_stop.png' );
  var horizontalPipeImage = require( 'image!SCENERY_PHET/faucet_horizontal_pipe.png' );
  var verticalPipeImage = require( 'image!SCENERY_PHET/faucet_vertical_pipe.png' );
  var bodyImage = require( 'image!SCENERY_PHET/faucet_body.png' );
  var spoutImage = require( 'image!SCENERY_PHET/faucet_spout.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var SPOUT_OUTPUT_CENTER_X = 112; // center of spout in bodyImage
  var HORIZONTAL_PIPE_Y_OFFSET = 30; // y-offset of horizontal pipe in bodyImage
  var HORIZONTAL_PIPE_X_OVERLAP = 1; // overlap between horizontal pipe and faucet body, so vertical seam is not visible
  var VERTICAL_PIPE_Y_OVERLAP = 1; // overlap between vertical pipe and faucet body/spout, so horizontal seam is not visible
  var SHOOTER_MIN_X_OFFSET = 4; // x-offset of shooter's off position in bodyImage
  var SHOOTER_MAX_X_OFFSET = 66; // x-offset of shooter's full-on position in bodyImage
  var SHOOTER_Y_OFFSET = 15; // y-offset of shooter's centerY in bodyImage
  var SHOOTER_WINDOW_BOUNDS = new Bounds2( 10, 10, 90, 25 ); // bounds of the window in bodyImage, through which you see the shooter handle

  /**
   * @param {Number} maxFlowRate
   * @param {Property<Number>} flowRateProperty
   * @param {Property<Boolean>} enabledProperty
   * @param {*} options
   * @constructor
   */
  function FaucetNode( maxFlowRate, flowRateProperty, enabledProperty, options ) {

    options = _.extend( {
      scale: 1,
      knobScale: 0.6, // values in the range 0.6 - 1.0 look decent
      horizontalPipeLength: 50, // distance between left edge of horizontal pipe and spout's center
      verticalPipeLength: 43, // length of the vertical pipe that connects the faucet body to the spout
      tapToDispenseEnabled: true, // tap-to-dispense feature: tapping the shooter dispenses some fluid
      tapToDispenseFlowRate: maxFlowRate / 2, // tap-to-dispense feature: flow rate, in L/sec
      tapToDispenseInterval: 500 // tap-to-dispense feature: amount of time that fluid is dispensed, in milliseconds
    }, options );

    var thisNode = this;
    Node.call( thisNode );

    // knob
    var knobNode = new Image( knobImage );
    var dx = 0.5 * knobNode.width;
    var dy = 0.5 * knobNode.height;
    knobNode.touchArea = Shape.rectangle( -dx, -dy, knobNode.width + dx + dx, knobNode.height + dy + dy ); // before scaling!
    knobNode.scale( options.knobScale );
    var knobDisabledNode = new Image( knobDisabledImage );
    knobDisabledNode.scale( knobNode.getScaleVector() );

    // shaft
    var shaftNode = new Image( shaftImage );

    // flange
    var flangeNode = new Image( flangeImage );
    var flangeDisabledNode = new Image( flangeDisabledImage );

    // stop
    var stopNode = new Image( stopImage );

    // assemble the shooter
    var shooterNode = new Node();
    shooterNode.addChild( shaftNode );
    shooterNode.addChild( stopNode );
    shooterNode.addChild( flangeNode );
    shooterNode.addChild( flangeDisabledNode );
    shooterNode.addChild( knobNode );
    shooterNode.addChild( knobDisabledNode );
    stopNode.x = shaftNode.x + 12;
    stopNode.centerY = shaftNode.centerY;
    flangeNode.left = shaftNode.right;
    flangeNode.centerY = shaftNode.centerY;
    flangeDisabledNode.x = flangeNode.x;
    flangeDisabledNode.y = flangeNode.y;
    knobNode.left = flangeNode.right - 8; // a bit of overlap makes this look better
    knobNode.centerY = flangeNode.centerY;
    knobDisabledNode.x = knobNode.x;
    knobDisabledNode.y = knobNode.y;

    // horizontal pipe, tiled horizontally
    var horizontalPipeNode = new Image( horizontalPipeImage );
    var horizontalPipeWidth = options.horizontalPipeLength - SPOUT_OUTPUT_CENTER_X + HORIZONTAL_PIPE_X_OVERLAP;
    assert && assert( horizontalPipeWidth > 0 );
    horizontalPipeNode.setScaleMagnitude( horizontalPipeWidth / horizontalPipeImage.width, 1 );

    // vertical pipe
    var verticalPipeNode = new Image( verticalPipeImage );
    var verticalPipeNodeHeight = options.verticalPipeLength + ( 2 * VERTICAL_PIPE_Y_OVERLAP );
    assert && assert( verticalPipeNodeHeight > 0 );
    verticalPipeNode.setScaleMagnitude( 1, verticalPipeNodeHeight / verticalPipeNode.height );

    // other nodes
    var spoutNode = new Image( spoutImage );
    var bodyNode = new Image( bodyImage );
    var shooterWindowNode = new Rectangle( SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.minY,
      SHOOTER_WINDOW_BOUNDS.maxX - SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.maxY - SHOOTER_WINDOW_BOUNDS.minY,
      { fill: 'rgb(107,107,107)' } );

    // rendering order
    thisNode.addChild( shooterWindowNode );
    thisNode.addChild( shooterNode );
    thisNode.addChild( horizontalPipeNode );
    thisNode.addChild( verticalPipeNode );
    thisNode.addChild( spoutNode );
    thisNode.addChild( bodyNode );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // layout
    {
      // spout's origin is at bottom-center
      spoutNode.centerX = 0;
      spoutNode.bottom = 0;
      // vertical pipe above spout
      verticalPipeNode.centerX = spoutNode.centerX;
      verticalPipeNode.bottom = spoutNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // body above vertical pipe
      bodyNode.right = verticalPipeNode.right;
      bodyNode.bottom = verticalPipeNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // shooter window is in the body's coordinate frame
      shooterWindowNode.x = bodyNode.x;
      shooterWindowNode.y = bodyNode.y;
      // horizontal pipe connects to left edge of body
      horizontalPipeNode.right = bodyNode.left + HORIZONTAL_PIPE_X_OVERLAP;
      horizontalPipeNode.top = bodyNode.top + HORIZONTAL_PIPE_Y_OFFSET;
      // shooter at top of body
      shooterNode.left = bodyNode.left + SHOOTER_MIN_X_OFFSET;
      shooterNode.centerY = bodyNode.top + SHOOTER_Y_OFFSET;
    }

    var offsetToFlowRate = new LinearFunction( SHOOTER_MIN_X_OFFSET, SHOOTER_MAX_X_OFFSET, 0, maxFlowRate, true /* clamp */ );

    // tap-to-dispense feature
    var timeoutID = null;
    var intervalID = null;
    var startTapToDispense = function() {
      flowRateProperty.set( options.tapToDispenseFlowRate );
      timeoutID = Timer.setTimeout( function() {
        intervalID = Timer.setInterval( function() {
          endTapToDispense();
        }, options.tapToDispenseInterval );
      }, 0 )
    };
    var endTapToDispense = function() {
      flowRateProperty.set( 0 );
      if ( timeoutID !== null ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID !== null ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
    };

    // encourage dragging by the blue parts, but make the entire shooter draggable
    var shooterHandler = new SimpleDragHandler( {

      startXOffset: 0, // where the drag started, relative to the target node's origin, in parent view coordinates
      dragged: false, // true if shooter was actually dragged during the drag sequence

      allowTouchSnag: true,

      start: function( event ) {
        this.dragged = false;
        this.startXOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).x;
      },

      // adjust the flow
      drag: function( event ) {
        if ( enabledProperty.get() ) {
          this.dragged = true;
          var xParent = event.currentTarget.globalToParentPoint( event.pointer.point ).x;
          var xOffset = xParent - this.startXOffset;
          var flowRate = offsetToFlowRate( xOffset );
          flowRateProperty.set( flowRate );
        }
      },

      end: function() {
        if ( this.dragged || !options.tapToDispenseEnabled ) {
          // turn off the faucet when the handle is released
          flowRateProperty.set( 0 );
        }
        else if ( enabledProperty.get() ) {
          startTapToDispense();
        }
      }
    } );
    shooterNode.addInputListener( shooterHandler );

    flowRateProperty.link( function( flowRate ) {
      var xOffset = offsetToFlowRate.inverse( flowRate );
      shooterNode.x = bodyNode.left + xOffset;
    } );

    enabledProperty.link( function( enabled ) {
      knobNode.cursor = flangeNode.cursor = enabled ? 'pointer' : 'default';
      knobNode.visible = enabled;
      knobDisabledNode.visible = !enabled;
      flangeNode.visible = enabled;
      flangeDisabledNode.visible = !enabled;
      if ( !enabled && shooterHandler.dragging ) {
        shooterHandler.endDrag();
      }
      if ( !enabled && options.tapToDispenseEnabled ) {
        endTapToDispense();
      }
    } );

    thisNode.mutate( options );
  }

  inherit( Node, FaucetNode );

  return FaucetNode;
} );
<commit_msg>lint fix<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Faucet with a pinball machine 'shooter'.
 * Pulling out the shooter changes the flow rate.
 * Releasing the shooter sets the flow rate to zero.
 * When the faucet is disabled, the flow rate is set to zero and the shooter is disabled.
 * Origin is at the bottom-center of the spout.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // imports
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Timer = require( 'JOIST/Timer' );

  // images
  var knobImage = require( 'image!SCENERY_PHET/faucet_knob.png' );
  var knobDisabledImage = require( 'image!SCENERY_PHET/faucet_knob_disabled.png' );
  var shaftImage = require( 'image!SCENERY_PHET/faucet_shaft.png' );
  var flangeImage = require( 'image!SCENERY_PHET/faucet_flange.png' );
  var flangeDisabledImage = require( 'image!SCENERY_PHET/faucet_flange_disabled.png' );
  var stopImage = require( 'image!SCENERY_PHET/faucet_stop.png' );
  var horizontalPipeImage = require( 'image!SCENERY_PHET/faucet_horizontal_pipe.png' );
  var verticalPipeImage = require( 'image!SCENERY_PHET/faucet_vertical_pipe.png' );
  var bodyImage = require( 'image!SCENERY_PHET/faucet_body.png' );
  var spoutImage = require( 'image!SCENERY_PHET/faucet_spout.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var SPOUT_OUTPUT_CENTER_X = 112; // center of spout in bodyImage
  var HORIZONTAL_PIPE_Y_OFFSET = 30; // y-offset of horizontal pipe in bodyImage
  var HORIZONTAL_PIPE_X_OVERLAP = 1; // overlap between horizontal pipe and faucet body, so vertical seam is not visible
  var VERTICAL_PIPE_Y_OVERLAP = 1; // overlap between vertical pipe and faucet body/spout, so horizontal seam is not visible
  var SHOOTER_MIN_X_OFFSET = 4; // x-offset of shooter's off position in bodyImage
  var SHOOTER_MAX_X_OFFSET = 66; // x-offset of shooter's full-on position in bodyImage
  var SHOOTER_Y_OFFSET = 15; // y-offset of shooter's centerY in bodyImage
  var SHOOTER_WINDOW_BOUNDS = new Bounds2( 10, 10, 90, 25 ); // bounds of the window in bodyImage, through which you see the shooter handle

  /**
   * @param {Number} maxFlowRate
   * @param {Property<Number>} flowRateProperty
   * @param {Property<Boolean>} enabledProperty
   * @param {*} options
   * @constructor
   */
  function FaucetNode( maxFlowRate, flowRateProperty, enabledProperty, options ) {

    options = _.extend( {
      scale: 1,
      knobScale: 0.6, // values in the range 0.6 - 1.0 look decent
      horizontalPipeLength: 50, // distance between left edge of horizontal pipe and spout's center
      verticalPipeLength: 43, // length of the vertical pipe that connects the faucet body to the spout
      tapToDispenseEnabled: true, // tap-to-dispense feature: tapping the shooter dispenses some fluid
      tapToDispenseFlowRate: maxFlowRate / 2, // tap-to-dispense feature: flow rate, in L/sec
      tapToDispenseInterval: 500 // tap-to-dispense feature: amount of time that fluid is dispensed, in milliseconds
    }, options );

    var thisNode = this;
    Node.call( thisNode );

    // knob
    var knobNode = new Image( knobImage );
    var dx = 0.5 * knobNode.width;
    var dy = 0.5 * knobNode.height;
    knobNode.touchArea = Shape.rectangle( -dx, -dy, knobNode.width + dx + dx, knobNode.height + dy + dy ); // before scaling!
    knobNode.scale( options.knobScale );
    var knobDisabledNode = new Image( knobDisabledImage );
    knobDisabledNode.scale( knobNode.getScaleVector() );

    // shaft
    var shaftNode = new Image( shaftImage );

    // flange
    var flangeNode = new Image( flangeImage );
    var flangeDisabledNode = new Image( flangeDisabledImage );

    // stop
    var stopNode = new Image( stopImage );

    // assemble the shooter
    var shooterNode = new Node();
    shooterNode.addChild( shaftNode );
    shooterNode.addChild( stopNode );
    shooterNode.addChild( flangeNode );
    shooterNode.addChild( flangeDisabledNode );
    shooterNode.addChild( knobNode );
    shooterNode.addChild( knobDisabledNode );
    stopNode.x = shaftNode.x + 12;
    stopNode.centerY = shaftNode.centerY;
    flangeNode.left = shaftNode.right;
    flangeNode.centerY = shaftNode.centerY;
    flangeDisabledNode.x = flangeNode.x;
    flangeDisabledNode.y = flangeNode.y;
    knobNode.left = flangeNode.right - 8; // a bit of overlap makes this look better
    knobNode.centerY = flangeNode.centerY;
    knobDisabledNode.x = knobNode.x;
    knobDisabledNode.y = knobNode.y;

    // horizontal pipe, tiled horizontally
    var horizontalPipeNode = new Image( horizontalPipeImage );
    var horizontalPipeWidth = options.horizontalPipeLength - SPOUT_OUTPUT_CENTER_X + HORIZONTAL_PIPE_X_OVERLAP;
    assert && assert( horizontalPipeWidth > 0 );
    horizontalPipeNode.setScaleMagnitude( horizontalPipeWidth / horizontalPipeImage.width, 1 );

    // vertical pipe
    var verticalPipeNode = new Image( verticalPipeImage );
    var verticalPipeNodeHeight = options.verticalPipeLength + ( 2 * VERTICAL_PIPE_Y_OVERLAP );
    assert && assert( verticalPipeNodeHeight > 0 );
    verticalPipeNode.setScaleMagnitude( 1, verticalPipeNodeHeight / verticalPipeNode.height );

    // other nodes
    var spoutNode = new Image( spoutImage );
    var bodyNode = new Image( bodyImage );
    var shooterWindowNode = new Rectangle( SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.minY,
      SHOOTER_WINDOW_BOUNDS.maxX - SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.maxY - SHOOTER_WINDOW_BOUNDS.minY,
      { fill: 'rgb(107,107,107)' } );

    // rendering order
    thisNode.addChild( shooterWindowNode );
    thisNode.addChild( shooterNode );
    thisNode.addChild( horizontalPipeNode );
    thisNode.addChild( verticalPipeNode );
    thisNode.addChild( spoutNode );
    thisNode.addChild( bodyNode );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // layout
    {
      // spout's origin is at bottom-center
      spoutNode.centerX = 0;
      spoutNode.bottom = 0;
      // vertical pipe above spout
      verticalPipeNode.centerX = spoutNode.centerX;
      verticalPipeNode.bottom = spoutNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // body above vertical pipe
      bodyNode.right = verticalPipeNode.right;
      bodyNode.bottom = verticalPipeNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // shooter window is in the body's coordinate frame
      shooterWindowNode.x = bodyNode.x;
      shooterWindowNode.y = bodyNode.y;
      // horizontal pipe connects to left edge of body
      horizontalPipeNode.right = bodyNode.left + HORIZONTAL_PIPE_X_OVERLAP;
      horizontalPipeNode.top = bodyNode.top + HORIZONTAL_PIPE_Y_OFFSET;
      // shooter at top of body
      shooterNode.left = bodyNode.left + SHOOTER_MIN_X_OFFSET;
      shooterNode.centerY = bodyNode.top + SHOOTER_Y_OFFSET;
    }

    var offsetToFlowRate = new LinearFunction( SHOOTER_MIN_X_OFFSET, SHOOTER_MAX_X_OFFSET, 0, maxFlowRate, true /* clamp */ );

    // tap-to-dispense feature
    var timeoutID = null;
    var intervalID = null;
    var startTapToDispense = function() {
      flowRateProperty.set( options.tapToDispenseFlowRate );
      timeoutID = Timer.setTimeout( function() {
        intervalID = Timer.setInterval( function() {
          endTapToDispense();
        }, options.tapToDispenseInterval );
      }, 0 );
    };
    var endTapToDispense = function() {
      flowRateProperty.set( 0 );
      if ( timeoutID !== null ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID !== null ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
    };

    // encourage dragging by the blue parts, but make the entire shooter draggable
    var shooterHandler = new SimpleDragHandler( {

      startXOffset: 0, // where the drag started, relative to the target node's origin, in parent view coordinates
      dragged: false, // true if shooter was actually dragged during the drag sequence

      allowTouchSnag: true,

      start: function( event ) {
        this.dragged = false;
        this.startXOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).x;
      },

      // adjust the flow
      drag: function( event ) {
        if ( enabledProperty.get() ) {
          this.dragged = true;
          var xParent = event.currentTarget.globalToParentPoint( event.pointer.point ).x;
          var xOffset = xParent - this.startXOffset;
          var flowRate = offsetToFlowRate( xOffset );
          flowRateProperty.set( flowRate );
        }
      },

      end: function() {
        if ( this.dragged || !options.tapToDispenseEnabled ) {
          // turn off the faucet when the handle is released
          flowRateProperty.set( 0 );
        }
        else if ( enabledProperty.get() ) {
          startTapToDispense();
        }
      }
    } );
    shooterNode.addInputListener( shooterHandler );

    flowRateProperty.link( function( flowRate ) {
      var xOffset = offsetToFlowRate.inverse( flowRate );
      shooterNode.x = bodyNode.left + xOffset;
    } );

    enabledProperty.link( function( enabled ) {
      knobNode.cursor = flangeNode.cursor = enabled ? 'pointer' : 'default';
      knobNode.visible = enabled;
      knobDisabledNode.visible = !enabled;
      flangeNode.visible = enabled;
      flangeDisabledNode.visible = !enabled;
      if ( !enabled && shooterHandler.dragging ) {
        shooterHandler.endDrag();
      }
      if ( !enabled && options.tapToDispenseEnabled ) {
        endTapToDispense();
      }
    } );

    thisNode.mutate( options );
  }

  inherit( Node, FaucetNode );

  return FaucetNode;
} );
<|endoftext|>


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

/**
 * Reward node that shows many nodes animating continuously, for fun!  Shown when a perfect score is achieved in a game.
 * You can also test this by running vegas/vegas_en.html and clicking on the "Reward" screen
 *
 * There are two ways to run the animation step function.  The client code can manually call step(dt), or the client code can pass in an Events instance that triggers events on 'step'.
 * In the latter case, the listener will automatically be removed when the animation is complete.
 *
 * @author Sam Reid
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var CanvasNode = require( 'SCENERY/nodes/CanvasNode' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var StarNode = require( 'SCENERY_PHET/StarNode' );
  var FaceNode = require( 'SCENERY_PHET/FaceNode' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Node = require( 'SCENERY/nodes/Node' );

  //This debug flag shows a gray rectangle for the CanvasNode to help ensure that its bounds are accurate
  var debug = false;

  function RewardNode( options ) {
    var rewardNode = this;
    this.options = options = _.extend( {

      //Bounds in which to render the canvas.  TODO: Should be full screen
      canvasBounds: new Bounds2( 0, 0, ScreenView.DEFAULT_LAYOUT_BOUNDS.width, ScreenView.DEFAULT_LAYOUT_BOUNDS.height ),

      //Scale things up for rasterization and back down for rendering so they have nice resolution on retina
      scaleForResolution: 2,

      //Nodes to appear in the reward node.  They will be cached as images to improve performance
      nodes: RewardNode.createRandomNodes( [
        new FaceNode( 40, {headStroke: 'black', headLineWidth: 1.5} ),
        new StarNode()
      ], 150 ),

      //If you pass in a stepSource, which conforms to the Events interface, the RewardNode will register for events through that source
      //TODO: Make it so the client doesn't pass in the entire model
      stepSource: null
    }, options );

    //If you pass in a stepSource, which conforms to the Events interface, the RewardNode will register for events through that source
    if ( options.stepSource ) {
      this.stepCallback = function( dt ) {rewardNode.step( dt );};
      options.stepSource.on( 'step', this.stepCallback );
    }

    //Cache each unique node as an image for faster rendering in canvas.  Use an intermediate imageWrapper since the images will be returned later asynchronously
    //And we need a place to store them, and know when they have arrived
    this.imageWrappers = [];

    //find the unique nodes in the array
    var uniqueNodes = _.uniq( this.options.nodes );

    uniqueNodes.forEach( function( node, i ) {
      rewardNode.imageWrappers.push(
        {

          //The image to be rendered in the canvas, will be filled in by toImage callback
          image: null,

          //Record the width and height so the nodes can be positioned before the toImage call has completed
          width: node.width,
          height: node.height,

          //The node itself is recorded in the wrapper so the wrapper can be looked up based on the original node
          node: node
        } );
      var parent = new Node( {children: [node], scale: options.scaleForResolution} );
      parent.toImage( function( image ) {
        rewardNode.imageWrappers[i].image = image;
      } );
    } );

    CanvasNode.call( this, options );

    //Some initialization must occur after this node is attached to the scene graph, see documentation for RewardNode.init below.
    this.inited = false;
  }

  return inherit( CanvasNode, RewardNode, {

      //Paint the rewards on the canvas
      // @param {CanvasContextWrapper} wrapper
      paintCanvas: function( wrapper ) {
        var context = wrapper.context;

        //If the debugging flag is on, show the bounds of the canvas
        if ( debug ) {
          var bounds = this.options.canvasBounds;

          //Fill the canvas with gray
          context.fillStyle = 'rgba(50,50,50,0.5)';
          context.fillRect( bounds.minX, bounds.minY, bounds.width, bounds.height );

          //Stroke the canvas border with blue
          context.strokeStyle = "#0000ff";
          context.lineWidth = 5;
          context.strokeRect( bounds.minX, bounds.minY, bounds.width, bounds.height );
        }
        context.scale( 1 / this.options.scaleForResolution, 1 / this.options.scaleForResolution );

        //Display the rewards, but check that they exist first.  They do not exist when attached to the timer with stepSource
        if ( this.rewards ) {
          for ( var i = 0; i < this.rewards.length; i++ ) {
            var reward = this.rewards[i];
            if ( reward.imageWrapper.image ) {
              context.drawImage( reward.imageWrapper.image, reward.x, reward.y );
            }
          }
        }
      },

      getScene: function() {
        //Find the root of the scene tree
        var node = this;
        while ( node ) {
          assert && assert( node._parents[1] === undefined, 'globalToLocalPoint unable to work for DAG' );
          if ( node._parents[0] ) {
            node = node._parents[0];
          }
          else {
            break;
          }
        }

        //TODO: add assertion to make sure node is a scenery.Scene
        return node;
      },

      //Find the first parent that is a ScreenView so we can listen for its transform, see https://github.com/phetsims/vegas/issues/4
      getScreenView: function() {
        var node = this;
        while ( node ) {
          assert && assert( node._parents[1] === undefined, 'globalToLocalPoint unable to work for DAG' );
          if ( node._parents[0] ) {
            node = node._parents[0];
            if ( node instanceof ScreenView ) {
              return node;
            }
          }
          else {
            break;
          }
        }

        throw new Error( 'No ScreenView found' );
      },

      //Only init after being attached to the scene graph, since we must ascertain the local bounds such that they take up the global screen.
      // 1. listen to the size of the scene/display
      // 2. record the trail between the scene and your CanvasNode, and
      // 3. apply the inverse of that transform to the CanvasNode (whenever an ancestor's transform changes, or when the scene/display size changes).
      //
      // JO said: for implementing now, I'd watch the iso transform, compute the inverse, and set bounds on changes to be precise (since you need them anyways to draw)
      init: function() {
        var rewardNode = this;
        var scene = this.getScene();

        //Listen to the bounds of the scene, so the canvas can be resized if the window is reshaped
        var updateBounds = function() {

          var local = rewardNode.globalToLocalBounds( scene.sceneBounds );
          rewardNode.setCanvasBounds( local );

          //Also, store the bounds in the options so the debug flag can render the bounds
          rewardNode.options.canvasBounds = local;
        };

        //When the scene is resized, update the bounds
        scene.addEventListener( 'resize', updateBounds );

        //When the ScreenView transform changes, update the bounds.  This prevents a "behind by one" problem, see https://github.com/phetsims/vegas/issues/4
        this.getScreenView().getTransform().addTransformListener( {before: function() {}, after: function() {updateBounds();}} );

        //Set the initial bounds
        updateBounds();

        //Store each reward, which has an imageWrapper (see above), x, y, speed
        this.rewards = [];
        for ( var i = 0; i < this.options.nodes.length; i++ ) {

          var node = this.options.nodes[i];
          (function( node ) {

            //find the image wrapper corresponding to the node
            var imageWrapper = _.find( rewardNode.imageWrappers, function( imageWrapper ) {return imageWrapper.node === node;} );
            rewardNode.rewards.push( {
              imageWrapper: imageWrapper,
              x: (Math.random() * rewardNode.options.canvasBounds.width + rewardNode.options.canvasBounds.left) * rewardNode.options.scaleForResolution - imageWrapper.width / 2,
              y: rewardNode.options.canvasBounds.top - Math.random() * rewardNode.options.canvasBounds.height * 2 - 200,
              speed: (Math.random() + 1) * 200
            } );
          })( node );
        }
      },

      //Move the rewards down according to their speed
      step: function( dt ) {
        if ( !this.inited && this.getScene() !== null ) {
          this.init();
          this.inited = true;
        }

        //Update all of the rewards
        var maxY = this.options.canvasBounds.height * this.options.scaleForResolution;
        for ( var i = 0; i < this.rewards.length; i++ ) {
          var reward = this.rewards[i];
          reward.y += reward.speed * dt;
          if ( reward.y > maxY ) {
            reward.y = this.options.canvasBounds.top - Math.random() * this.options.canvasBounds.height * 2 - 200;
          }
        }
        this.invalidatePaint();
      }
    },

    //Static methods
    {
      /**
       * Convenience factory method to create an array of the specified nodes in an even distribution.
       * @param {Array[Node]} nodes array of nodes to use
       * @param {Number} count
       */
      createRandomNodes: function( nodes, count ) {
        var array = [];
        for ( var i = 0; i < count; i++ ) {
          array.push( nodes[i % nodes.length] );
        }
        return array;
      }
    } );
} );<commit_msg>moved canvas bounds out of options, see #4<commit_after>//  Copyright 2002-2014, University of Colorado Boulder

/**
 * Reward node that shows many nodes animating continuously, for fun!  Shown when a perfect score is achieved in a game.
 * You can also test this by running vegas/vegas_en.html and clicking on the "Reward" screen
 *
 * There are two ways to run the animation step function.  The client code can manually call step(dt), or the client code can pass in an Events instance that triggers events on 'step'.
 * In the latter case, the listener will automatically be removed when the animation is complete.
 *
 * @author Sam Reid
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var CanvasNode = require( 'SCENERY/nodes/CanvasNode' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var StarNode = require( 'SCENERY_PHET/StarNode' );
  var FaceNode = require( 'SCENERY_PHET/FaceNode' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Node = require( 'SCENERY/nodes/Node' );

  //This debug flag shows a gray rectangle for the CanvasNode to help ensure that its bounds are accurate
  var debug = false;

  function RewardNode( options ) {
    var rewardNode = this;

    //Bounds in which to render the canvas.  Should be the full screen.  See below for how this is computed based on ScreenView bounds and relative transforms
    this.canvasDisplayBounds = new Bounds2( 0, 0, ScreenView.DEFAULT_LAYOUT_BOUNDS.width, ScreenView.DEFAULT_LAYOUT_BOUNDS.height );

    this.options = options = _.extend( {

      //Scale things up for rasterization and back down for rendering so they have nice resolution on retina
      scaleForResolution: 2,

      //Nodes to appear in the reward node.  They will be cached as images to improve performance
      nodes: RewardNode.createRandomNodes( [
        new FaceNode( 40, {headStroke: 'black', headLineWidth: 1.5} ),
        new StarNode()
      ], 150 ),

      //If you pass in a stepSource, which conforms to the Events interface, the RewardNode will register for events through that source
      //TODO: Make it so the client doesn't pass in the entire model
      stepSource: null
    }, options );

    //If you pass in a stepSource, which conforms to the Events interface, the RewardNode will register for events through that source
    if ( options.stepSource ) {
      this.stepCallback = function( dt ) {rewardNode.step( dt );};
      options.stepSource.on( 'step', this.stepCallback );
    }

    //Cache each unique node as an image for faster rendering in canvas.  Use an intermediate imageWrapper since the images will be returned later asynchronously
    //And we need a place to store them, and know when they have arrived
    this.imageWrappers = [];

    //find the unique nodes in the array
    var uniqueNodes = _.uniq( this.options.nodes );

    uniqueNodes.forEach( function( node, i ) {
      rewardNode.imageWrappers.push(
        {

          //The image to be rendered in the canvas, will be filled in by toImage callback
          image: null,

          //Record the width and height so the nodes can be positioned before the toImage call has completed
          width: node.width,
          height: node.height,

          //The node itself is recorded in the wrapper so the wrapper can be looked up based on the original node
          node: node
        } );
      var parent = new Node( {children: [node], scale: options.scaleForResolution} );
      parent.toImage( function( image ) {
        rewardNode.imageWrappers[i].image = image;
      } );
    } );

    CanvasNode.call( this, options );

    //Some initialization must occur after this node is attached to the scene graph, see documentation for RewardNode.init below.
    this.inited = false;
  }

  return inherit( CanvasNode, RewardNode, {

      //Paint the rewards on the canvas
      // @param {CanvasContextWrapper} wrapper
      paintCanvas: function( wrapper ) {
        var context = wrapper.context;

        //If the debugging flag is on, show the bounds of the canvas
        if ( debug ) {
          var bounds = this.options.canvasDisplayBounds;

          //Fill the canvas with gray
          context.fillStyle = 'rgba(50,50,50,0.5)';
          context.fillRect( bounds.minX, bounds.minY, bounds.width, bounds.height );

          //Stroke the canvas border with blue
          context.strokeStyle = "#0000ff";
          context.lineWidth = 5;
          context.strokeRect( bounds.minX, bounds.minY, bounds.width, bounds.height );
        }
        context.scale( 1 / this.options.scaleForResolution, 1 / this.options.scaleForResolution );

        //Display the rewards, but check that they exist first.  They do not exist when attached to the timer with stepSource
        if ( this.rewards ) {
          for ( var i = 0; i < this.rewards.length; i++ ) {
            var reward = this.rewards[i];
            if ( reward.imageWrapper.image ) {
              context.drawImage( reward.imageWrapper.image, reward.x, reward.y );
            }
          }
        }
      },

      getScene: function() {
        //Find the root of the scene tree
        var node = this;
        while ( node ) {
          assert && assert( node._parents[1] === undefined, 'globalToLocalPoint unable to work for DAG' );
          if ( node._parents[0] ) {
            node = node._parents[0];
          }
          else {
            break;
          }
        }

        //TODO: add assertion to make sure node is a scenery.Scene
        return node;
      },

      //Find the first parent that is a ScreenView so we can listen for its transform, see https://github.com/phetsims/vegas/issues/4
      getScreenView: function() {
        var node = this;
        while ( node ) {
          assert && assert( node._parents[1] === undefined, 'globalToLocalPoint unable to work for DAG' );
          if ( node._parents[0] ) {
            node = node._parents[0];
            if ( node instanceof ScreenView ) {
              return node;
            }
          }
          else {
            break;
          }
        }

        throw new Error( 'No ScreenView found' );
      },

      //Only init after being attached to the scene graph, since we must ascertain the local bounds such that they take up the global screen.
      // 1. listen to the size of the scene/display
      // 2. record the trail between the scene and your CanvasNode, and
      // 3. apply the inverse of that transform to the CanvasNode (whenever an ancestor's transform changes, or when the scene/display size changes).
      //
      // JO said: for implementing now, I'd watch the iso transform, compute the inverse, and set bounds on changes to be precise (since you need them anyways to draw)
      init: function() {
        var rewardNode = this;
        var scene = this.getScene();

        //Listen to the bounds of the scene, so the canvas can be resized if the window is reshaped
        var updateBounds = function() {

          var local = rewardNode.globalToLocalBounds( scene.sceneBounds );
          rewardNode.setCanvasBounds( local );

          //Also, store the bounds in the options so the debug flag can render the bounds
          rewardNode.canvasDisplayBounds = local;
        };

        //When the scene is resized, update the bounds
        scene.addEventListener( 'resize', updateBounds );

        //When the ScreenView transform changes, update the bounds.  This prevents a "behind by one" problem, see https://github.com/phetsims/vegas/issues/4
        this.getScreenView().getTransform().addTransformListener( {before: function() {}, after: function() {updateBounds();}} );

        //Set the initial bounds
        updateBounds();

        //Store each reward, which has an imageWrapper (see above), x, y, speed
        this.rewards = [];
        for ( var i = 0; i < this.options.nodes.length; i++ ) {

          var node = this.options.nodes[i];
          (function( node ) {

            //find the image wrapper corresponding to the node
            var imageWrapper = _.find( rewardNode.imageWrappers, function( imageWrapper ) {return imageWrapper.node === node;} );
            rewardNode.rewards.push( {
              imageWrapper: imageWrapper,
              x: (Math.random() * rewardNode.canvasDisplayBounds.width + rewardNode.canvasDisplayBounds.left) * rewardNode.options.scaleForResolution - imageWrapper.width / 2,
              y: rewardNode.canvasDisplayBounds.top - Math.random() * rewardNode.canvasDisplayBounds.height * 2 - 200,
              speed: (Math.random() + 1) * 200
            } );
          })( node );
        }
      },

      //Move the rewards down according to their speed
      step: function( dt ) {
        if ( !this.inited && this.getScene() !== null ) {
          this.init();
          this.inited = true;
        }

        //Update all of the rewards
        var maxY = this.canvasDisplayBounds.height * this.options.scaleForResolution;
        for ( var i = 0; i < this.rewards.length; i++ ) {
          var reward = this.rewards[i];
          reward.y += reward.speed * dt;
          if ( reward.y > maxY ) {
            reward.y = this.canvasDisplayBounds.top - Math.random() * this.canvasDisplayBounds.height * 2 - 200;
          }
        }
        this.invalidatePaint();
      }
    },

    //Static methods
    {
      /**
       * Convenience factory method to create an array of the specified nodes in an even distribution.
       * @param {Array[Node]} nodes array of nodes to use
       * @param {Number} count
       */
      createRandomNodes: function( nodes, count ) {
        var array = [];
        for ( var i = 0; i < count; i++ ) {
          array.push( nodes[i % nodes.length] );
        }
        return array;
      }
    } );
} );<|endoftext|>