text: /* wax - 5.0.0-alpha2 - 1.0.4-496-g1d0b3d8 */
  
  
  /*!
    * Reqwest! A general purpose XHR connection manager
    * copyright Dustin Diaz 2011
    * https://github.com/ded/reqwest
    * license MIT
    */
  !function(context,win){function serial(a){var b=a.name;if(a.disabled||!b)return"";b=enc(b);switch(a.tagName.toLowerCase()){case"input":switch(a.type){case"reset":case"button":case"image":case"file":return"";case"checkbox":case"radio":return a.checked?b+"="+(a.value?enc(a.value):!0)+"&":"";default:return b+"="+(a.value?enc(a.value):"")+"&"}break;case"textarea":return b+"="+enc(a.value)+"&";case"select":return b+"="+enc(a.options[a.selectedIndex].value)+"&"}return""}function enc(a){return encodeURIComponent(a)}function reqwest(a,b){return new Reqwest(a,b)}function init(o,fn){function error(a){o.error&&o.error(a),complete(a)}function success(resp){o.timeout&&clearTimeout(self.timeout)&&(self.timeout=null);var r=resp.responseText;if(r)switch(type){case"json":resp=win.JSON?win.JSON.parse(r):eval("("+r+")");break;case"js":resp=eval(r);break;case"html":resp=r}fn(resp),o.success&&o.success(resp),complete(resp)}function complete(a){o.complete&&o.complete(a)}this.url=typeof o=="string"?o:o.url,this.timeout=null;var type=o.type||setType(this.url),self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){self.abort(),error()},o.timeout)),this.request=getRequest(o,success,error)}function setType(a){if(/\.json$/.test(a))return"json";if(/\.jsonp$/.test(a))return"jsonp";if(/\.js$/.test(a))return"js";if(/\.html?$/.test(a))return"html";if(/\.xml$/.test(a))return"xml";return"js"}function Reqwest(a,b){this.o=a,this.fn=b,init.apply(this,arguments)}function getRequest(a,b,c){if(a.type!="jsonp"){var f=xhr();f.open(a.method||"GET",typeof a=="string"?a:a.url,!0),setHeaders(f,a),f.onreadystatechange=handleReadyState(f,b,c),a.before&&a.before(f),f.send(a.data||null);return f}var d=doc.createElement("script"),e=0;win[getCallbackName(a)]=generalCallback,d.type="text/javascript",d.src=a.url,d.async=!0,d.onload=d.onreadystatechange=function(){if(d[readyState]&&d[readyState]!=="complete"&&d[readyState]!=="loaded"||e)return!1;d.onload=d.onreadystatechange=null,a.success&&a.success(lastValue),lastValue=undefined,head.removeChild(d),e=1},head.appendChild(d)}function generalCallback(a){lastValue=a}function getCallbackName(a){var b=a.jsonpCallback||"callback";if(a.url.slice(-(b.length+2))==b+"=?"){var c="reqwest_"+uniqid++;a.url=a.url.substr(0,a.url.length-1)+c;return c}var d=new RegExp(b+"=([\\w]+)");return a.url.match(d)[1]}function setHeaders(a,b){var c=b.headers||{};c.Accept=c.Accept||"text/javascript, text/html, application/xml, text/xml, */*",b.crossOrigin||(c["X-Requested-With"]=c["X-Requested-With"]||"XMLHttpRequest"),c[contentType]=c[contentType]||"application/x-www-form-urlencoded";for(var d in c)c.hasOwnProperty(d)&&a.setRequestHeader(d,c[d],!1)}function handleReadyState(a,b,c){return function(){a&&a[readyState]==4&&(twoHundo.test(a.status)?b(a):c(a))}}var twoHundo=/^20\d$/,doc=document,byTag="getElementsByTagName",readyState="readyState",contentType="Content-Type",head=doc[byTag]("head")[0],uniqid=0,lastValue,xhr="XMLHttpRequest"in win?function(){return new XMLHttpRequest}:function(){return new ActiveXObject("Microsoft.XMLHTTP")};Reqwest.prototype={abort:function(){this.request.abort()},retry:function(){init.call(this,this.o,this.fn)}},reqwest.serialize=function(a){var b=[a[byTag]("input"),a[byTag]("select"),a[byTag]("textarea")],c=[],d,e;for(d=0,l=b.length;d<l;++d)for(e=0,l2=b[d].length;e<l2;++e)c.push(serial(b[d][e]));return c.join("").replace(/&$/,"")},reqwest.serializeArray=function(a){for(var b=this.serialize(a).split("&"),c=0,d=b.length,e=[],f;c<d;c++)b[c]&&(f=b[c].split("="))&&e.push({name:f[0],value:f[1]});return e};var old=context.reqwest;reqwest.noConflict=function(){context.reqwest=old;return this},typeof module!="undefined"?module.exports=reqwest:context.reqwest=reqwest}(this,window)// Copyright Google Inc.
  // Licensed under the Apache Licence Version 2.0
  // Autogenerated at Tue Oct 11 13:36:46 EDT 2011
  // @provides html4
  var html4 = {};
  html4.atype = {
    NONE: 0,
    URI: 1,
    URI_FRAGMENT: 11,
    SCRIPT: 2,
    STYLE: 3,
    ID: 4,
    IDREF: 5,
    IDREFS: 6,
    GLOBAL_NAME: 7,
    LOCAL_NAME: 8,
    CLASSES: 9,
    FRAME_TARGET: 10
  };
  html4.ATTRIBS = {
    '*::class': 9,
    '*::dir': 0,
    '*::id': 4,
    '*::lang': 0,
    '*::onclick': 2,
    '*::ondblclick': 2,
    '*::onkeydown': 2,
    '*::onkeypress': 2,
    '*::onkeyup': 2,
    '*::onload': 2,
    '*::onmousedown': 2,
    '*::onmousemove': 2,
    '*::onmouseout': 2,
    '*::onmouseover': 2,
    '*::onmouseup': 2,
    '*::style': 3,
    '*::title': 0,
    'a::accesskey': 0,
    'a::coords': 0,
    'a::href': 1,
    'a::hreflang': 0,
    'a::name': 7,
    'a::onblur': 2,
    'a::onfocus': 2,
    'a::rel': 0,
    'a::rev': 0,
    'a::shape': 0,
    'a::tabindex': 0,
    'a::target': 10,
    'a::type': 0,
    'area::accesskey': 0,
    'area::alt': 0,
    'area::coords': 0,
    'area::href': 1,
    'area::nohref': 0,
    'area::onblur': 2,
    'area::onfocus': 2,
    'area::shape': 0,
    'area::tabindex': 0,
    'area::target': 10,
    'bdo::dir': 0,
    'blockquote::cite': 1,
    'br::clear': 0,
    'button::accesskey': 0,
    'button::disabled': 0,
    'button::name': 8,
    'button::onblur': 2,
    'button::onfocus': 2,
    'button::tabindex': 0,
    'button::type': 0,
    'button::value': 0,
    'canvas::height': 0,
    'canvas::width': 0,
    'caption::align': 0,
    'col::align': 0,
    'col::char': 0,
    'col::charoff': 0,
    'col::span': 0,
    'col::valign': 0,
    'col::width': 0,
    'colgroup::align': 0,
    'colgroup::char': 0,
    'colgroup::charoff': 0,
    'colgroup::span': 0,
    'colgroup::valign': 0,
    'colgroup::width': 0,
    'del::cite': 1,
    'del::datetime': 0,
    'dir::compact': 0,
    'div::align': 0,
    'dl::compact': 0,
    'font::color': 0,
    'font::face': 0,
    'font::size': 0,
    'form::accept': 0,
    'form::action': 1,
    'form::autocomplete': 0,
    'form::enctype': 0,
    'form::method': 0,
    'form::name': 7,
    'form::onreset': 2,
    'form::onsubmit': 2,
    'form::target': 10,
    'h1::align': 0,
    'h2::align': 0,
    'h3::align': 0,
    'h4::align': 0,
    'h5::align': 0,
    'h6::align': 0,
    'hr::align': 0,
    'hr::noshade': 0,
    'hr::size': 0,
    'hr::width': 0,
    'iframe::align': 0,
    'iframe::frameborder': 0,
    'iframe::height': 0,
    'iframe::marginheight': 0,
    'iframe::marginwidth': 0,
    'iframe::width': 0,
    'img::align': 0,
    'img::alt': 0,
    'img::border': 0,
    'img::height': 0,
    'img::hspace': 0,
    'img::ismap': 0,
    'img::name': 7,
    'img::src': 1,
    'img::usemap': 11,
    'img::vspace': 0,
    'img::width': 0,
    'input::accept': 0,
    'input::accesskey': 0,
    'input::align': 0,
    'input::alt': 0,
    'input::autocomplete': 0,
    'input::checked': 0,
    'input::disabled': 0,
    'input::ismap': 0,
    'input::maxlength': 0,
    'input::name': 8,
    'input::onblur': 2,
    'input::onchange': 2,
    'input::onfocus': 2,
    'input::onselect': 2,
    'input::readonly': 0,
    'input::size': 0,
    'input::src': 1,
    'input::tabindex': 0,
    'input::type': 0,
    'input::usemap': 11,
    'input::value': 0,
    'ins::cite': 1,
    'ins::datetime': 0,
    'label::accesskey': 0,
    'label::for': 5,
    'label::onblur': 2,
    'label::onfocus': 2,
    'legend::accesskey': 0,
    'legend::align': 0,
    'li::type': 0,
    'li::value': 0,
    'map::name': 7,
    'menu::compact': 0,
    'ol::compact': 0,
    'ol::start': 0,
    'ol::type': 0,
    'optgroup::disabled': 0,
    'optgroup::label': 0,
    'option::disabled': 0,
    'option::label': 0,
    'option::selected': 0,
    'option::value': 0,
    'p::align': 0,
    'pre::width': 0,
    'q::cite': 1,
    'select::disabled': 0,
    'select::multiple': 0,
    'select::name': 8,
    'select::onblur': 2,
    'select::onchange': 2,
    'select::onfocus': 2,
    'select::size': 0,
    'select::tabindex': 0,
    'table::align': 0,
    'table::bgcolor': 0,
    'table::border': 0,
    'table::cellpadding': 0,
    'table::cellspacing': 0,
    'table::frame': 0,
    'table::rules': 0,
    'table::summary': 0,
    'table::width': 0,
    'tbody::align': 0,
    'tbody::char': 0,
    'tbody::charoff': 0,
    'tbody::valign': 0,
    'td::abbr': 0,
    'td::align': 0,
    'td::axis': 0,
    'td::bgcolor': 0,
    'td::char': 0,
    'td::charoff': 0,
    'td::colspan': 0,
    'td::headers': 6,
    'td::height': 0,
    'td::nowrap': 0,
    'td::rowspan': 0,
    'td::scope': 0,
    'td::valign': 0,
    'td::width': 0,
    'textarea::accesskey': 0,
    'textarea::cols': 0,
    'textarea::disabled': 0,
    'textarea::name': 8,
    'textarea::onblur': 2,
    'textarea::onchange': 2,
    'textarea::onfocus': 2,
    'textarea::onselect': 2,
    'textarea::readonly': 0,
    'textarea::rows': 0,
    'textarea::tabindex': 0,
    'tfoot::align': 0,
    'tfoot::char': 0,
    'tfoot::charoff': 0,
    'tfoot::valign': 0,
    'th::abbr': 0,
    'th::align': 0,
    'th::axis': 0,
    'th::bgcolor': 0,
    'th::char': 0,
    'th::charoff': 0,
    'th::colspan': 0,
    'th::headers': 6,
    'th::height': 0,
    'th::nowrap': 0,
    'th::rowspan': 0,
    'th::scope': 0,
    'th::valign': 0,
    'th::width': 0,
    'thead::align': 0,
    'thead::char': 0,
    'thead::charoff': 0,
    'thead::valign': 0,
    'tr::align': 0,
    'tr::bgcolor': 0,
    'tr::char': 0,
    'tr::charoff': 0,
    'tr::valign': 0,
    'ul::compact': 0,
    'ul::type': 0
  };
  html4.eflags = {
    OPTIONAL_ENDTAG: 1,
    EMPTY: 2,
    CDATA: 4,
    RCDATA: 8,
    UNSAFE: 16,
    FOLDABLE: 32,
    SCRIPT: 64,
    STYLE: 128
  };
  html4.ELEMENTS = {
    'a': 0,
    'abbr': 0,
    'acronym': 0,
    'address': 0,
    'applet': 16,
    'area': 2,
    'b': 0,
    'base': 18,
    'basefont': 18,
    'bdo': 0,
    'big': 0,
    'blockquote': 0,
    'body': 49,
    'br': 2,
    'button': 0,
    'canvas': 0,
    'caption': 0,
    'center': 0,
    'cite': 0,
    'code': 0,
    'col': 2,
    'colgroup': 1,
    'dd': 1,
    'del': 0,
    'dfn': 0,
    'dir': 0,
    'div': 0,
    'dl': 0,
    'dt': 1,
    'em': 0,
    'fieldset': 0,
    'font': 0,
    'form': 0,
    'frame': 18,
    'frameset': 16,
    'h1': 0,
    'h2': 0,
    'h3': 0,
    'h4': 0,
    'h5': 0,
    'h6': 0,
    'head': 49,
    'hr': 2,
    'html': 49,
    'i': 0,
    'iframe': 4,
    'img': 2,
    'input': 2,
    'ins': 0,
    'isindex': 18,
    'kbd': 0,
    'label': 0,
    'legend': 0,
    'li': 1,
    'link': 18,
    'map': 0,
    'menu': 0,
    'meta': 18,
    'nobr': 0,
    'noembed': 4,
    'noframes': 20,
    'noscript': 20,
    'object': 16,
    'ol': 0,
    'optgroup': 0,
    'option': 1,
    'p': 1,
    'param': 18,
    'pre': 0,
    'q': 0,
    's': 0,
    'samp': 0,
    'script': 84,
    'select': 0,
    'small': 0,
    'span': 0,
    'strike': 0,
    'strong': 0,
    'style': 148,
    'sub': 0,
    'sup': 0,
    'table': 0,
    'tbody': 1,
    'td': 1,
    'textarea': 8,
    'tfoot': 1,
    'th': 1,
    'thead': 1,
    'title': 24,
    'tr': 1,
    'tt': 0,
    'u': 0,
    'ul': 0,
    'var': 0
  };
  html4.ueffects = {
    NOT_LOADED: 0,
    SAME_DOCUMENT: 1,
    NEW_DOCUMENT: 2
  };
  html4.URIEFFECTS = {
    'a::href': 2,
    'area::href': 2,
    'blockquote::cite': 0,
    'body::background': 1,
    'del::cite': 0,
    'form::action': 2,
    'img::src': 1,
    'input::src': 1,
    'ins::cite': 0,
    'q::cite': 0
  };
  html4.ltypes = {
    UNSANDBOXED: 2,
    SANDBOXED: 1,
    DATA: 0
  };
  html4.LOADERTYPES = {
    'a::href': 2,
    'area::href': 2,
    'blockquote::cite': 2,
    'body::background': 1,
    'del::cite': 2,
    'form::action': 2,
    'img::src': 1,
    'input::src': 1,
    'ins::cite': 2,
    'q::cite': 2
  };;
  // Copyright (C) 2006 Google Inc.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileoverview
   * An HTML sanitizer that can satisfy a variety of security policies.
   *
   * <p>
   * The HTML sanitizer is built around a SAX parser and HTML element and
   * attributes schemas.
   *
   * @author mikesamuel@gmail.com
   * @requires html4
   * @overrides window
   * @provides html, html_sanitize
   */
  
  /**
   * @namespace
   */
  var html = (function (html4) {
    var lcase;
    // The below may not be true on browsers in the Turkish locale.
    if ('script' === 'SCRIPT'.toLowerCase()) {
      lcase = function (s) { return s.toLowerCase(); };
    } else {
      /**
       * {@updoc
       * $ lcase('SCRIPT')
       * # 'script'
       * $ lcase('script')
       * # 'script'
       * }
       */
      lcase = function (s) {
        return s.replace(
            /[A-Z]/g,
            function (ch) {
              return String.fromCharCode(ch.charCodeAt(0) | 32);
            });
      };
    }
  
    var ENTITIES = {
      lt   : '<',
      gt   : '>',
      amp  : '&',
      nbsp : '\240',
      quot : '"',
      apos : '\''
    };
    
    // Schemes on which to defer to uripolicy. Urls with other schemes are denied
    var WHITELISTED_SCHEMES = /^(?:https?|mailto|data)$/i;
  
    var decimalEscapeRe = /^#(\d+)$/;
    var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/;
    /**
     * Decodes an HTML entity.
     *
     * {@updoc
     * $ lookupEntity('lt')
     * # '<'
     * $ lookupEntity('GT')
     * # '>'
     * $ lookupEntity('amp')
     * # '&'
     * $ lookupEntity('nbsp')
     * # '\xA0'
     * $ lookupEntity('apos')
     * # "'"
     * $ lookupEntity('quot')
     * # '"'
     * $ lookupEntity('#xa')
     * # '\n'
     * $ lookupEntity('#10')
     * # '\n'
     * $ lookupEntity('#x0a')
     * # '\n'
     * $ lookupEntity('#010')
     * # '\n'
     * $ lookupEntity('#x00A')
     * # '\n'
     * $ lookupEntity('Pi')      // Known failure
     * # '\u03A0'
     * $ lookupEntity('pi')      // Known failure
     * # '\u03C0'
     * }
     *
     * @param name the content between the '&' and the ';'.
     * @return a single unicode code-point as a string.
     */
    function lookupEntity(name) {
      name = lcase(name);  // TODO: &pi; is different from &Pi;
      if (ENTITIES.hasOwnProperty(name)) { return ENTITIES[name]; }
      var m = name.match(decimalEscapeRe);
      if (m) {
        return String.fromCharCode(parseInt(m[1], 10));
      } else if (!!(m = name.match(hexEscapeRe))) {
        return String.fromCharCode(parseInt(m[1], 16));
      }
      return '';
    }
  
    function decodeOneEntity(_, name) {
      return lookupEntity(name);
    }
  
    var nulRe = /\0/g;
    function stripNULs(s) {
      return s.replace(nulRe, '');
    }
  
    var entityRe = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g;
    /**
     * The plain text of a chunk of HTML CDATA which possibly containing.
     *
     * {@updoc
     * $ unescapeEntities('')
     * # ''
     * $ unescapeEntities('hello World!')
     * # 'hello World!'
     * $ unescapeEntities('1 &lt; 2 &amp;&AMP; 4 &gt; 3&#10;')
     * # '1 < 2 && 4 > 3\n'
     * $ unescapeEntities('&lt;&lt <- unfinished entity&gt;')
     * # '<&lt <- unfinished entity>'
     * $ unescapeEntities('/foo?bar=baz&copy=true')  // & often unescaped in URLS
     * # '/foo?bar=baz&copy=true'
     * $ unescapeEntities('pi=&pi;&#x3c0;, Pi=&Pi;\u03A0') // FIXME: known failure
     * # 'pi=\u03C0\u03c0, Pi=\u03A0\u03A0'
     * }
     *
     * @param s a chunk of HTML CDATA.  It must not start or end inside an HTML
     *   entity.
     */
    function unescapeEntities(s) {
      return s.replace(entityRe, decodeOneEntity);
    }
  
    var ampRe = /&/g;
    var looseAmpRe = /&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi;
    var ltRe = /</g;
    var gtRe = />/g;
    var quotRe = /\"/g;
    var eqRe = /\=/g;  // Backslash required on JScript.net
  
    /**
     * Escapes HTML special characters in attribute values as HTML entities.
     *
     * {@updoc
     * $ escapeAttrib('')
     * # ''
     * $ escapeAttrib('"<<&==&>>"')  // Do not just escape the first occurrence.
     * # '&#34;&lt;&lt;&amp;&#61;&#61;&amp;&gt;&gt;&#34;'
     * $ escapeAttrib('Hello <World>!')
     * # 'Hello &lt;World&gt;!'
     * }
     */
    function escapeAttrib(s) {
      // Escaping '=' defangs many UTF-7 and SGML short-tag attacks.
      return s.replace(ampRe, '&amp;').replace(ltRe, '&lt;').replace(gtRe, '&gt;')
          .replace(quotRe, '&#34;').replace(eqRe, '&#61;');
    }
  
    /**
     * Escape entities in RCDATA that can be escaped without changing the meaning.
     * {@updoc
     * $ normalizeRCData('1 < 2 &&amp; 3 > 4 &amp;& 5 &lt; 7&8')
     * # '1 &lt; 2 &amp;&amp; 3 &gt; 4 &amp;&amp; 5 &lt; 7&amp;8'
     * }
     */
    function normalizeRCData(rcdata) {
      return rcdata
          .replace(looseAmpRe, '&amp;$1')
          .replace(ltRe, '&lt;')
          .replace(gtRe, '&gt;');
    }
  
  
    // TODO(mikesamuel): validate sanitizer regexs against the HTML5 grammar at
    // http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html
    // http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html
    // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html
    // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html
  
    /** token definitions. */
    var INSIDE_TAG_TOKEN = new RegExp(
        // Don't capture space.
        '^\\s*(?:'
        // Capture an attribute name in group 1, and value in group 3.
        // We capture the fact that there was an attribute in group 2, since
        // interpreters are inconsistent in whether a group that matches nothing
        // is null, undefined, or the empty string.
        + ('(?:'
           + '([a-z][a-z-]*)'                    // attribute name
           + ('('                                // optionally followed
              + '\\s*=\\s*'
              + ('('
                 // A double quoted string.
                 + '\"[^\"]*\"'
                 // A single quoted string.
                 + '|\'[^\']*\''
                 // The positive lookahead is used to make sure that in
                 // <foo bar= baz=boo>, the value for bar is blank, not "baz=boo".
                 + '|(?=[a-z][a-z-]*\\s*=)'
                 // An unquoted value that is not an attribute name.
                 // We know it is not an attribute name because the previous
                 // zero-width match would've eliminated that possibility.
                 + '|[^>\"\'\\s]*'
                 + ')'
                 )
              + ')'
              ) + '?'
           + ')'
           )
        // End of tag captured in group 3.
        + '|(\/?>)'
        // Don't capture cruft
        + '|[\\s\\S][^a-z\\s>]*)',
        'i');
  
    var OUTSIDE_TAG_TOKEN = new RegExp(
        '^(?:'
        // Entity captured in group 1.
        + '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);'
        // Comment, doctypes, and processing instructions not captured.
        + '|<\!--[\\s\\S]*?--\>|<!\\w[^>]*>|<\\?[^>*]*>'
        // '/' captured in group 2 for close tags, and name captured in group 3.
        + '|<(\/)?([a-z][a-z0-9]*)'
        // Text captured in group 4.
        + '|([^<&>]+)'
        // Cruft captured in group 5.
        + '|([<&>]))',
        'i');
  
    /**
     * Given a SAX-like event handler, produce a function that feeds those
     * events and a parameter to the event handler.
     *
     * The event handler has the form:{@code
     * {
     *   // Name is an upper-case HTML tag name.  Attribs is an array of
     *   // alternating upper-case attribute names, and attribute values.  The
     *   // attribs array is reused by the parser.  Param is the value passed to
     *   // the saxParser.
     *   startTag: function (name, attribs, param) { ... },
     *   endTag:   function (name, param) { ... },
     *   pcdata:   function (text, param) { ... },
     *   rcdata:   function (text, param) { ... },
     *   cdata:    function (text, param) { ... },
     *   startDoc: function (param) { ... },
     *   endDoc:   function (param) { ... }
     * }}
     *
     * @param {Object} handler a record containing event handlers.
     * @return {Function} that takes a chunk of html and a parameter.
     *   The parameter is passed on to the handler methods.
     */
    function makeSaxParser(handler) {
      return function parse(htmlText, param) {
        htmlText = String(htmlText);
        var htmlLower = null;
  
        var inTag = false;  // True iff we're currently processing a tag.
        var attribs = [];  // Accumulates attribute names and values.
        var tagName = void 0;  // The name of the tag currently being processed.
        var eflags = void 0;  // The element flags for the current tag.
        var openTag = void 0;  // True if the current tag is an open tag.
  
        if (handler.startDoc) { handler.startDoc(param); }
  
        while (htmlText) {
          var m = htmlText.match(inTag ? INSIDE_TAG_TOKEN : OUTSIDE_TAG_TOKEN);
          htmlText = htmlText.substring(m[0].length);
  
          if (inTag) {
            if (m[1]) { // attribute
              // setAttribute with uppercase names doesn't work on IE6.
              var attribName = lcase(m[1]);
              var decodedValue;
              if (m[2]) {
                var encodedValue = m[3];
                switch (encodedValue.charCodeAt(0)) {  // Strip quotes
                  case 34: case 39:
                    encodedValue = encodedValue.substring(
                        1, encodedValue.length - 1);
                    break;
                }
                decodedValue = unescapeEntities(stripNULs(encodedValue));
              } else {
                // Use name as value for valueless attribs, so
                //   <input type=checkbox checked>
                // gets attributes ['type', 'checkbox', 'checked', 'checked']
                decodedValue = attribName;
              }
              attribs.push(attribName, decodedValue);
            } else if (m[4]) {
              if (eflags !== void 0) {  // False if not in whitelist.
                if (openTag) {
                  if (handler.startTag) {
                    handler.startTag(tagName, attribs, param);
                  }
                } else {
                  if (handler.endTag) {
                    handler.endTag(tagName, param);
                  }
                }
              }
  
              if (openTag
                  && (eflags & (html4.eflags.CDATA | html4.eflags.RCDATA))) {
                if (htmlLower === null) {
                  htmlLower = lcase(htmlText);
                } else {
                  htmlLower = htmlLower.substring(
                      htmlLower.length - htmlText.length);
                }
                var dataEnd = htmlLower.indexOf('</' + tagName);
                if (dataEnd < 0) { dataEnd = htmlText.length; }
                if (dataEnd) {
                  if (eflags & html4.eflags.CDATA) {
                    if (handler.cdata) {
                      handler.cdata(htmlText.substring(0, dataEnd), param);
                    }
                  } else if (handler.rcdata) {
                    handler.rcdata(
                      normalizeRCData(htmlText.substring(0, dataEnd)), param);
                  }
                  htmlText = htmlText.substring(dataEnd);
                }
              }
  
              tagName = eflags = openTag = void 0;
              attribs.length = 0;
              inTag = false;
            }
          } else {
            if (m[1]) {  // Entity
              if (handler.pcdata) { handler.pcdata(m[0], param); }
            } else if (m[3]) {  // Tag
              openTag = !m[2];
              inTag = true;
              tagName = lcase(m[3]);
              eflags = html4.ELEMENTS.hasOwnProperty(tagName)
                  ? html4.ELEMENTS[tagName] : void 0;
            } else if (m[4]) {  // Text
              if (handler.pcdata) { handler.pcdata(m[4], param); }
            } else if (m[5]) {  // Cruft
              if (handler.pcdata) {
                var ch = m[5];
                handler.pcdata(
                    ch === '<' ? '&lt;' : ch === '>' ? '&gt;' : '&amp;',
                    param);
              }
            }
          }
        }
  
        if (handler.endDoc) { handler.endDoc(param); }
      };
    }
  
    /**
     * Returns a function that strips unsafe tags and attributes from html.
     * @param {Function} sanitizeAttributes
     *     maps from (tagName, attribs[]) to null or a sanitized attribute array.
     *     The attribs array can be arbitrarily modified, but the same array
     *     instance is reused, so should not be held.
     * @return {Function} from html to sanitized html
     */
    function makeHtmlSanitizer(sanitizeAttributes) {
      var stack;
      var ignoring;
      return makeSaxParser({
          startDoc: function (_) {
            stack = [];
            ignoring = false;
          },
          startTag: function (tagName, attribs, out) {
            if (ignoring) { return; }
            if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
            var eflags = html4.ELEMENTS[tagName];
            if (eflags & html4.eflags.FOLDABLE) {
              return;
            } else if (eflags & html4.eflags.UNSAFE) {
              ignoring = !(eflags & html4.eflags.EMPTY);
              return;
            }
            attribs = sanitizeAttributes(tagName, attribs);
            // TODO(mikesamuel): relying on sanitizeAttributes not to
            // insert unsafe attribute names.
            if (attribs) {
              if (!(eflags & html4.eflags.EMPTY)) {
                stack.push(tagName);
              }
  
              out.push('<', tagName);
              for (var i = 0, n = attribs.length; i < n; i += 2) {
                var attribName = attribs[i],
                    value = attribs[i + 1];
                if (value !== null && value !== void 0) {
                  out.push(' ', attribName, '="', escapeAttrib(value), '"');
                }
              }
              out.push('>');
            }
          },
          endTag: function (tagName, out) {
            if (ignoring) {
              ignoring = false;
              return;
            }
            if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
            var eflags = html4.ELEMENTS[tagName];
            if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY
                            | html4.eflags.FOLDABLE))) {
              var index;
              if (eflags & html4.eflags.OPTIONAL_ENDTAG) {
                for (index = stack.length; --index >= 0;) {
                  var stackEl = stack[index];
                  if (stackEl === tagName) { break; }
                  if (!(html4.ELEMENTS[stackEl]
                        & html4.eflags.OPTIONAL_ENDTAG)) {
                    // Don't pop non optional end tags looking for a match.
                    return;
                  }
                }
              } else {
                for (index = stack.length; --index >= 0;) {
                  if (stack[index] === tagName) { break; }
                }
              }
              if (index < 0) { return; }  // Not opened.
              for (var i = stack.length; --i > index;) {
                var stackEl = stack[i];
                if (!(html4.ELEMENTS[stackEl]
                      & html4.eflags.OPTIONAL_ENDTAG)) {
                  out.push('</', stackEl, '>');
                }
              }
              stack.length = index;
              out.push('</', tagName, '>');
            }
          },
          pcdata: function (text, out) {
            if (!ignoring) { out.push(text); }
          },
          rcdata: function (text, out) {
            if (!ignoring) { out.push(text); }
          },
          cdata: function (text, out) {
            if (!ignoring) { out.push(text); }
          },
          endDoc: function (out) {
            for (var i = stack.length; --i >= 0;) {
              out.push('</', stack[i], '>');
            }
            stack.length = 0;
          }
        });
    }
  
    // From RFC3986
    var URI_SCHEME_RE = new RegExp(
          "^" +
        "(?:" +
          "([^:\/?#]+)" +         // scheme
        ":)?"
        );
  
    /**
     * Strips unsafe tags and attributes from html.
     * @param {string} htmlText to sanitize
     * @param {Function} opt_uriPolicy -- a transform to apply to uri/url
     *     attribute values.  If no opt_uriPolicy is provided, no uris
     *     are allowed ie. the default uriPolicy rewrites all uris to null
     * @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to
     *     apply to names, ids, and classes. If no opt_nmTokenPolicy is provided,
     *     all names, ids and classes are passed through ie. the default
     *     nmTokenPolicy is an identity transform
     * @return {string} html
     */
    function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) {
      var out = [];
      makeHtmlSanitizer(
        function sanitizeAttribs(tagName, attribs) {
          for (var i = 0; i < attribs.length; i += 2) {
            var attribName = attribs[i];
            var value = attribs[i + 1];
            var atype = null, attribKey;
            if ((attribKey = tagName + '::' + attribName,
                 html4.ATTRIBS.hasOwnProperty(attribKey))
                || (attribKey = '*::' + attribName,
                    html4.ATTRIBS.hasOwnProperty(attribKey))) {
              atype = html4.ATTRIBS[attribKey];
            }
            if (atype !== null) {
              switch (atype) {
                case html4.atype.NONE: break;
                case html4.atype.SCRIPT:
                case html4.atype.STYLE:
                  value = null;
                  break;
                case html4.atype.ID:
                case html4.atype.IDREF:
                case html4.atype.IDREFS:
                case html4.atype.GLOBAL_NAME:
                case html4.atype.LOCAL_NAME:
                case html4.atype.CLASSES:
                  value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
                  break;
                case html4.atype.URI:
                  var parsedUri = ('' + value).match(URI_SCHEME_RE);
                  if (!parsedUri) {
                    value = null;
                  } else if (!parsedUri[1] ||
                      WHITELISTED_SCHEMES.test(parsedUri[1])) {
                    value = opt_uriPolicy && opt_uriPolicy(value);
                  } else {
                    value = null;
                  }
                  break;
                case html4.atype.URI_FRAGMENT:
                  if (value && '#' === value.charAt(0)) {
                    value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
                    if (value) { value = '#' + value; }
                  } else {
                    value = null;
                  }
                  break;
                default:
                  value = null;
                  break;
              }
            } else {
              value = null;
            }
            attribs[i + 1] = value;
          }
          return attribs;
        })(htmlText, out);
      return out.join('');
    }
  
    return {
      escapeAttrib: escapeAttrib,
      makeHtmlSanitizer: makeHtmlSanitizer,
      makeSaxParser: makeSaxParser,
      normalizeRCData: normalizeRCData,
      sanitize: sanitize,
      unescapeEntities: unescapeEntities
    };
  })(html4);
  
  var html_sanitize = html.sanitize;
  
  // Exports for closure compiler.  Note this file is also cajoled
  // for domado and run in an environment without 'window'
  if (typeof window !== 'undefined') {
    window['html'] = html;
    window['html_sanitize'] = html_sanitize;
  }
  // Loosen restrictions of Caja's
  // html-sanitizer to allow for styling
  html4.ATTRIBS['*::style'] = 0;
  html4.ELEMENTS['style'] = 0;
  /*
    mustache.js — Logic-less templates in JavaScript
  
    See http://mustache.github.com/ for more info.
  */
  
  var Mustache = function() {
    var regexCache = {};
    var Renderer = function() {};
  
    Renderer.prototype = {
      otag: "{{",
      ctag: "}}",
      pragmas: {},
      buffer: [],
      pragmas_implemented: {
        "IMPLICIT-ITERATOR": true
      },
      context: {},
  
      render: function(template, context, partials, in_recursion) {
        // reset buffer & set context
        if(!in_recursion) {
          this.context = context;
          this.buffer = []; // TODO: make this non-lazy
        }
  
        // fail fast
        if(!this.includes("", template)) {
          if(in_recursion) {
            return template;
          } else {
            this.send(template);
            return;
          }
        }
  
        // get the pragmas together
        template = this.render_pragmas(template);
  
        // render the template
        var html = this.render_section(template, context, partials);
  
        // render_section did not find any sections, we still need to render the tags
        if (html === false) {
          html = this.render_tags(template, context, partials, in_recursion);
        }
  
        if (in_recursion) {
          return html;
        } else {
          this.sendLines(html);
        }
      },
  
      /*
        Sends parsed lines
      */
      send: function(line) {
        if(line !== "") {
          this.buffer.push(line);
        }
      },
  
      sendLines: function(text) {
        if (text) {
          var lines = text.split("\n");
          for (var i = 0; i < lines.length; i++) {
            this.send(lines[i]);
          }
        }
      },
  
      /*
        Looks for %PRAGMAS
      */
      render_pragmas: function(template) {
        // no pragmas
        if(!this.includes("%", template)) {
          return template;
        }
  
        var that = this;
        var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) {
          return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g");
        });
  
        return template.replace(regex, function(match, pragma, options) {
          if(!that.pragmas_implemented[pragma]) {
            throw({message:
              "This implementation of mustache doesn't understand the '" +
              pragma + "' pragma"});
          }
          that.pragmas[pragma] = {};
          if(options) {
            var opts = options.split("=");
            that.pragmas[pragma][opts[0]] = opts[1];
          }
          return "";
          // ignore unknown pragmas silently
        });
      },
  
      /*
        Tries to find a partial in the curent scope and render it
      */
      render_partial: function(name, context, partials) {
        name = this.trim(name);
        if(!partials || partials[name] === undefined) {
          throw({message: "unknown_partial '" + name + "'"});
        }
        if(typeof(context[name]) != "object") {
          return this.render(partials[name], context, partials, true);
        }
        return this.render(partials[name], context[name], partials, true);
      },
  
      /*
        Renders inverted (^) and normal (#) sections
      */
      render_section: function(template, context, partials) {
        if(!this.includes("#", template) && !this.includes("^", template)) {
          // did not render anything, there were no sections
          return false;
        }
  
        var that = this;
  
        var regex = this.getCachedRegex("render_section", function(otag, ctag) {
          // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder
          return new RegExp(
            "^([\\s\\S]*?)" +         // all the crap at the beginning that is not {{*}} ($1)
  
            otag +                    // {{
            "(\\^|\\#)\\s*(.+)\\s*" + //  #foo (# == $2, foo == $3)
            ctag +                    // }}
  
            "\n*([\\s\\S]*?)" +       // between the tag ($2). leading newlines are dropped
  
            otag +                    // {{
            "\\/\\s*\\3\\s*" +        //  /foo (backreference to the opening tag).
            ctag +                    // }}
  
            "\\s*([\\s\\S]*)$",       // everything else in the string ($4). leading whitespace is dropped.
  
          "g");
        });
  
  
        // for each {{#foo}}{{/foo}} section do...
        return template.replace(regex, function(match, before, type, name, content, after) {
          // before contains only tags, no sections
          var renderedBefore = before ? that.render_tags(before, context, partials, true) : "",
  
          // after may contain both sections and tags, so use full rendering function
              renderedAfter = after ? that.render(after, context, partials, true) : "",
  
          // will be computed below
              renderedContent,
  
              value = that.find(name, context);
  
          if (type === "^") { // inverted section
            if (!value || that.is_array(value) && value.length === 0) {
              // false or empty list, render it
              renderedContent = that.render(content, context, partials, true);
            } else {
              renderedContent = "";
            }
          } else if (type === "#") { // normal section
            if (that.is_array(value)) { // Enumerable, Let's loop!
              renderedContent = that.map(value, function(row) {
                return that.render(content, that.create_context(row), partials, true);
              }).join("");
            } else if (that.is_object(value)) { // Object, Use it as subcontext!
              renderedContent = that.render(content, that.create_context(value),
                partials, true);
            } else if (typeof value === "function") {
              // higher order section
              renderedContent = value.call(context, content, function(text) {
                return that.render(text, context, partials, true);
              });
            } else if (value) { // boolean section
              renderedContent = that.render(content, context, partials, true);
            } else {
              renderedContent = "";
            }
          }
  
          return renderedBefore + renderedContent + renderedAfter;
        });
      },
  
      /*
        Replace {{foo}} and friends with values from our view
      */
      render_tags: function(template, context, partials, in_recursion) {
        // tit for tat
        var that = this;
  
  
  
        var new_regex = function() {
          return that.getCachedRegex("render_tags", function(otag, ctag) {
            return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g");
          });
        };
  
        var regex = new_regex();
        var tag_replace_callback = function(match, operator, name) {
          switch(operator) {
          case "!": // ignore comments
            return "";
          case "=": // set new delimiters, rebuild the replace regexp
            that.set_delimiters(name);
            regex = new_regex();
            return "";
          case ">": // render partial
            return that.render_partial(name, context, partials);
          case "{": // the triple mustache is unescaped
            return that.find(name, context);
          default: // escape the value
            return that.escape(that.find(name, context));
          }
        };
        var lines = template.split("\n");
        for(var i = 0; i < lines.length; i++) {
          lines[i] = lines[i].replace(regex, tag_replace_callback, this);
          if(!in_recursion) {
            this.send(lines[i]);
          }
        }
  
        if(in_recursion) {
          return lines.join("\n");
        }
      },
  
      set_delimiters: function(delimiters) {
        var dels = delimiters.split(" ");
        this.otag = this.escape_regex(dels[0]);
        this.ctag = this.escape_regex(dels[1]);
      },
  
      escape_regex: function(text) {
        // thank you Simon Willison
        if(!arguments.callee.sRE) {
          var specials = [
            '/', '.', '*', '+', '?', '|',
            '(', ')', '[', ']', '{', '}', '\\'
          ];
          arguments.callee.sRE = new RegExp(
            '(\\' + specials.join('|\\') + ')', 'g'
          );
        }
        return text.replace(arguments.callee.sRE, '\\$1');
      },
  
      /*
        find `name` in current `context`. That is find me a value
        from the view object
      */
      find: function(name, context) {
        name = this.trim(name);
  
        // Checks whether a value is thruthy or false or 0
        function is_kinda_truthy(bool) {
          return bool === false || bool === 0 || bool;
        }
  
        var value;
        if(is_kinda_truthy(context[name])) {
          value = context[name];
        } else if(is_kinda_truthy(this.context[name])) {
          value = this.context[name];
        }
  
        if(typeof value === "function") {
          return value.apply(context);
        }
        if(value !== undefined) {
          return value;
        }
        // silently ignore unkown variables
        return "";
      },
  
      // Utility methods
  
      /* includes tag */
      includes: function(needle, haystack) {
        return haystack.indexOf(this.otag + needle) != -1;
      },
  
      /*
        Does away with nasty characters
      */
      escape: function(s) {
        s = String(s === null ? "" : s);
        return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
          switch(s) {
          case "&": return "&amp;";
          case '"': return '&quot;';
          case "'": return '&#39;';
          case "<": return "&lt;";
          case ">": return "&gt;";
          default: return s;
          }
        });
      },
  
      // by @langalex, support for arrays of strings
      create_context: function(_context) {
        if(this.is_object(_context)) {
          return _context;
        } else {
          var iterator = ".";
          if(this.pragmas["IMPLICIT-ITERATOR"]) {
            iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
          }
          var ctx = {};
          ctx[iterator] = _context;
          return ctx;
        }
      },
  
      is_object: function(a) {
        return a && typeof a == "object";
      },
  
      is_array: function(a) {
        return Object.prototype.toString.call(a) === '[object Array]';
      },
  
      /*
        Gets rid of leading and trailing whitespace
      */
      trim: function(s) {
        return s.replace(/^\s*|\s*$/g, "");
      },
  
      /*
        Why, why, why? Because IE. Cry, cry cry.
      */
      map: function(array, fn) {
        if (typeof array.map == "function") {
          return array.map(fn);
        } else {
          var r = [];
          var l = array.length;
          for(var i = 0; i < l; i++) {
            r.push(fn(array[i]));
          }
          return r;
        }
      },
  
      getCachedRegex: function(name, generator) {
        var byOtag = regexCache[this.otag];
        if (!byOtag) {
          byOtag = regexCache[this.otag] = {};
        }
  
        var byCtag = byOtag[this.ctag];
        if (!byCtag) {
          byCtag = byOtag[this.ctag] = {};
        }
  
        var regex = byCtag[name];
        if (!regex) {
          regex = byCtag[name] = generator(this.otag, this.ctag);
        }
  
        return regex;
      }
    };
  
    return({
      name: "mustache.js",
      version: "0.4.0-dev",
  
      /*
        Turns a template and view into HTML
      */
      to_html: function(template, view, partials, send_fun) {
        var renderer = new Renderer();
        if(send_fun) {
          renderer.send = send_fun;
        }
        renderer.render(template, view || {}, partials);
        if(!send_fun) {
          return renderer.buffer.join("\n");
        }
      }
    });
  }();
  var wax = wax || {};
  wax.ol = wax.ol || {};
  
  wax.ol.connector = function(tilejson) {
      for (var i = 0; i < tilejson.tiles.length; i++) {
          tilejson.tiles[i] = tilejson.tiles[i]
              .replace('{z}', '${z}')
              .replace('{x}', '${x}')
              .replace('{y}', '${y}');
      }
      return new OpenLayers.Layer.XYZ(
          tilejson.name,
          tilejson.tiles, {
              sphericalMercator: true,
              zoomOffset: tilejson.minzoom,
              numZoomLevels: tilejson.maxzoom - tilejson.minzoom,
              attribution: tilejson.attribution
          });
  };
  ;wax = wax || {};
  
  // Attribution
  // -----------
  wax.attribution = function() {
      var container,
          a = {};
  
      function urlX(url) {
          // Data URIs are subject to a bug in Firefox
          // https://bugzilla.mozilla.org/show_bug.cgi?id=255107
          // which let them be a vector. But WebKit does 'the right thing'
          // or at least 'something' about this situation, so we'll tolerate
          // them.
          if (/^(https?:\/\/|data:image)/.test(url)) {
              return url;
          }
      }
  
      function idX(id) {
          return id;
      }
  
      a.set = function(content) {
          if (typeof content === 'undefined') return;
          container.innerHTML = html_sanitize(content, urlX, idX);
          return this;
      };
  
      a.element = function() {
          return container;
      };
  
      a.init = function() {
          container = document.createElement('div');
          container.className = 'wax-attribution';
          return this;
      };
  
      return a.init();
  };
  wax = wax || {};
  
  // Attribution
  // -----------
  wax.bwdetect = function(options, callback) {
      var detector = {},
          threshold = options.threshold || 400,
          // test image: 30.29KB
          testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()),
          // High-bandwidth assumed
          // 1: high bandwidth (.png, .jpg)
          // 0: low bandwidth (.png128, .jpg70)
          bw = 1,
          // Alternative versions
          auto = options.auto === undefined ? true : options.auto;
  
      function bwTest() {
          wax.bw = -1;
          var im = new Image();
          im.src = testImage;
          var first = true;
          var timeout = setTimeout(function() {
              if (first && wax.bw == -1) {
                  detector.bw(0);
                  first = false;
              }
          }, threshold);
          im.onload = function() {
              if (first && wax.bw == -1) {
                  clearTimeout(timeout);
                  detector.bw(1);
                  first = false;
              }
          };
      }
  
      detector.bw = function(x) {
          if (!arguments.length) return bw;
          var oldBw = bw;
          if (wax.bwlisteners && wax.bwlisteners.length) (function () {
              listeners = wax.bwlisteners;
              wax.bwlisteners = [];
              for (i = 0; i < listeners; i++) {
                  listeners[i](x);
              }
          })();
          wax.bw = x;
  
          if (bw != (bw = x)) callback(x);
      };
  
      detector.add = function() {
          if (auto) bwTest();
          return this;
      };
  
      if (wax.bw == -1) {
        wax.bwlisteners = wax.bwlisteners || [];
        wax.bwlisteners.push(detector.bw);
      } else if (wax.bw !== undefined) {
          detector.bw(wax.bw);
      } else {
          detector.add();
      }
      return detector;
  };
  // Formatter
  // ---------
  //
  // This code is no longer the recommended code path for Wax -
  // see `template.js`, a safe implementation of Mustache templates.
  wax.formatter = function(x) {
      var formatter = {},
          f;
  
      // Prevent against just any input being used.
      if (x && typeof x === 'string') {
          try {
              // Ugly, dangerous use of eval.
              eval('f = ' + x);
          } catch (e) {
              if (console) console.log(e);
          }
      } else if (x && typeof x === 'function') {
          f = x;
      } else {
          f = function() {};
      }
  
      function urlX(url) {
          if (/^(https?:\/\/|data:image)/.test(url)) {
              return url;
          }
      }
  
      function idX(id) {
          return id;
      }
  
      // Wrap the given formatter function in order to
      // catch exceptions that it may throw.
      formatter.format = function(options, data) {
          try {
              return html_sanitize(f(options, data), urlX, idX);
          } catch (e) {
              if (console) console.log(e);
          }
      };
  
      return formatter;
  };
  // GridInstance
  // ------------
  // GridInstances are queryable, fully-formed
  // objects for acquiring features from events.
  //
  // This code ignores format of 1.1-1.2
  wax.GridInstance = function(grid_tile, formatter, options) {
      options = options || {};
      // resolution is the grid-elements-per-pixel ratio of gridded data.
      // The size of a tile element. For now we expect tiles to be squares.
      var instance = {},
          resolution = options.resolution || 4,
          tileSize = options.tileSize || 256;
  
      // Resolve the UTF-8 encoding stored in grids to simple
      // number values.
      // See the [utfgrid spec](https://github.com/mapbox/utfgrid-spec)
      // for details.
      function resolveCode(key) {
          if (key >= 93) key--;
          if (key >= 35) key--;
          key -= 32;
          return key;
      }
  
      instance.grid_tile = function() {
          return grid_tile;
      };
  
      instance.getKey = function(x, y) {
          if (!(grid_tile && grid_tile.grid)) return;
          if ((y < 0) || (x < 0)) return;
          if ((Math.floor(y) >= tileSize) ||
              (Math.floor(x) >= tileSize)) return;
          // Find the key in the grid. The above calls should ensure that
          // the grid's array is large enough to make this work.
          return resolveCode(grid_tile.grid[
             Math.floor((y) / resolution)
          ].charCodeAt(
             Math.floor((x) / resolution)
          ));
      };
  
      // Lower-level than tileFeature - has nothing to do
      // with the DOM. Takes a px offset from 0, 0 of a grid.
      instance.gridFeature = function(x, y) {
          // Find the key in the grid. The above calls should ensure that
          // the grid's array is large enough to make this work.
          var key = this.getKey(x, y),
              keys = grid_tile.keys;
  
          if (keys &&
              keys[key] &&
              grid_tile.data[keys[key]]) {
              return grid_tile.data[keys[key]];
          }
      };
  
      // Get a feature:
      //
      // * `x` and `y`: the screen coordinates of an event
      // * `tile_element`: a DOM element of a tile, from which we can get an offset.
      // * `options` options to give to the formatter: minimally having a `format`
      //   member, being `full`, `teaser`, or something else.
      instance.tileFeature = function(x, y, tile_element, options) {
          if (!grid_tile) return;
          // IE problem here - though recoverable, for whatever reason
          var offset = wax.util.offset(tile_element);
              feature = this.gridFeature(x - offset.left, y - offset.top);
  
          if (feature) return formatter.format(options, feature);
      };
  
      return instance;
  };
  // GridManager
  // -----------
  // Generally one GridManager will be used per map.
  //
  // It takes one options object, which current accepts a single option:
  // `resolution` determines the number of pixels per grid element in the grid.
  // The default is 4.
  wax.GridManager = function(options) {
      options = options || {};
  
      var resolution = options.resolution || 4,
          version = options.version || '1.1',
          grid_tiles = {},
          manager = {},
          formatter;
  
      var gridUrl = function(url) {
          return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json');
      };
  
      function templatedGridUrl(template) {
          if (typeof template === 'string') template = [template];
          return function templatedGridFinder(url) {
              if (!url) return;
              var xyz = /(\d+)\/(\d+)\/(\d+)\.[\w\._]+/g.exec(url);
              if (!xyz) return;
              return template[parseInt(xyz[2], 10) % template.length]
                  .replace('{z}', xyz[1])
                  .replace('{x}', xyz[2])
                  .replace('{y}', xyz[3]);
          };
      }
  
      manager.formatter = function(x) {
          if (!arguments.length) return formatter;
          formatter =  wax.formatter(x);
          return manager;
      };
  
      manager.template = function(x) {
          if (!arguments.length) return formatter;
          formatter = wax.template(x);
          return manager;
      };
  
      manager.gridUrl = function(x) {
          if (!arguments.length) return gridUrl;
          gridUrl = typeof x === 'function' ?
              x : templatedGridUrl(x);
          return manager;
      };
  
      manager.getGrid = function(url, callback) {
          var gurl = gridUrl(url);
          if (!formatter || !gurl) return callback(null, null);
  
          wax.request.get(gurl, function(err, t) {
              if (err) return callback(err, null);
              callback(null, wax.GridInstance(t, formatter, {
                  resolution: resolution || 4
              }));
          });
          return manager;
      };
  
      manager.add = function(options) {
          if (options.template) {
              manager.template(options.template);
          } else if (options.formatter) {
              manager.formatter(options.formatter);
          }
  
          if (options.grids) {
              manager.gridUrl(options.grids);
          }
          return this;
      };
  
      return manager.add(options);
  };
  wax = wax || {};
  
  // Hash
  // ----
  wax.hash = function(options) {
      options = options || {};
  
      function getState() {
          return location.hash.substring(1);
      }
  
      function pushState(state) {
          location.hash = '#' + state;
      }
  
      var s0, // old hash
          hash = {},
          lat = 90 - 1e-8;  // allowable latitude range
  
      function parseHash(s) {
          var args = s.split('/');
          for (var i = 0; i < args.length; i++) {
              args[i] = Number(args[i]);
              if (isNaN(args[i])) return true;
          }
          if (args.length < 3) {
              // replace bogus hash
              return true;
          } else if (args.length == 3) {
              options.setCenterZoom(args);
          }
      }
  
      function move() {
          var s1 = options.getCenterZoom();
          if (s0 !== s1) {
              s0 = s1;
              // don't recenter the map!
              pushState(s0);
          }
      }
  
      function stateChange(state) {
          // ignore spurious hashchange events
          if (state === s0) return;
          if (parseHash(s0 = state)) {
              // replace bogus hash
              move();
          }
      }
  
      var _move = wax.util.throttle(move, 500);
  
      hash.add = function() {
          stateChange(getState());
          options.bindChange(_move);
          return this;
      };
  
      hash.remove = function() {
          options.unbindChange(_move);
          return this;
      };
  
      return hash.add();
  };
  // Wax Legend
  // ----------
  
  // Wax header
  var wax = wax || {};
  
  wax.legend = function() {
      var element,
          legend = {},
          container;
  
      function urlX(url) {
          // Data URIs are subject to a bug in Firefox
          // https://bugzilla.mozilla.org/show_bug.cgi?id=255107
          // which let them be a vector. But WebKit does 'the right thing'
          // or at least 'something' about this situation, so we'll tolerate
          // them.
          if (/^(https?:\/\/|data:image)/.test(url)) {
              return url;
          }
      }
  
      function idX(id) {
          return id;
      }
  
      legend.element = function() {
          return container;
      };
  
      legend.content = function(content) {
          if (!arguments.length) return element.innerHTML;
          if (content) {
              element.innerHTML = html_sanitize(content, urlX, idX);
              element.style.display = 'block';
          } else {
              element.innerHTML = '';
              element.style.display = 'none';
          }
          return this;
      };
  
      legend.add = function() {
          container = document.createElement('div');
          container.className = 'wax-legends';
  
          element = document.createElement('div');
          element.className = 'wax-legend';
          element.style.display = 'none';
  
          container.appendChild(element);
          return this;
      };
  
      return legend.add();
  };
  // Like underscore's bind, except it runs a function
  // with no arguments off of an object.
  //
  //     var map = ...;
  //     w(map).melt(myFunction);
  //
  // is equivalent to
  //
  //     var map = ...;
  //     myFunction(map);
  //
  var w = function(self) {
      self.melt = function(func, obj) {
          return func.apply(obj, [self, obj]);
      };
      return self;
  };
  var wax = wax || {};
  wax.movetip = {};
  
  wax.movetip = function(options) {
      options = options || {};
      var t = {},
          _currentTooltip = undefined,
          _context = undefined,
          _animationOut = options.animationOut,
          _animationIn = options.animationIn;
  
      // Helper function to determine whether a given element is a wax popup.
      function isPopup (el) {
          return el && el.className.indexOf('wax-popup') !== -1;
      }
  
      function getTooltip(feature, context) {
          var tooltip = document.createElement('div');
          tooltip.className = 'wax-movetip';
          tooltip.style.cssText = 'position:absolute;'
          tooltip.innerHTML = feature;
          context.appendChild(tooltip);
          _context = context;
          _tooltipOffset = wax.util.offset(tooltip);
          _contextOffset = wax.util.offset(_context);
          return tooltip;
      }
  
      function moveTooltip(e) {
          if (!_currentTooltip) return;
          var eo = wax.util.eventoffset(e);
  
          _currentTooltip.className = 'wax-movetip';
  
          // faux-positioning
          if ((_tooltipOffset.height + eo.y) >
              (_contextOffset.top + _contextOffset.height) &&
              (_contextOffset.height > _tooltipOffset.height)) {
              eo.y -= _tooltipOffset.height;
              _currentTooltip.className += ' flip-y';
          }
  
          // faux-positioning
          if ((_tooltipOffset.width + eo.x) >
              (_contextOffset.left + _contextOffset.width)) {
              eo.x -= _tooltipOffset.width;
              _currentTooltip.className += ' flip-x';
          }
  
          _currentTooltip.style.left = eo.x + 'px';
          _currentTooltip.style.top = eo.y + 'px';
      }
  
      // Hide a given tooltip.
      function hideTooltip(el) {
          if (!el) return;
          var event,
              remove = function() {
              if (this.parentNode) this.parentNode.removeChild(this);
          };
  
          if (el.style['-webkit-transition'] !== undefined && _animationOut) {
              event = 'webkitTransitionEnd';
          } else if (el.style.MozTransition !== undefined && _animationOut) {
              event = 'transitionend';
          }
  
          if (event) {
              // This code assumes that transform-supporting browsers
              // also support proper events. IE9 does both.
              el.addEventListener(event, remove, false);
              el.addEventListener('transitionend', remove, false);
              el.className += ' ' + _animationOut;
          } else {
              if (el.parentNode) el.parentNode.removeChild(el);
          }
      }
  
      // Expand a tooltip to be a "popup". Suspends all other tooltips from being
      // shown until this popup is closed or another popup is opened.
      function click(feature, context) {
          // Hide any current tooltips.
          if (_currentTooltip) {
              hideTooltip(_currentTooltip);
              _currentTooltip = undefined;
          }
  
          var tooltip = getTooltip(feature, context);
          tooltip.className += ' wax-popup';
          tooltip.innerHTML = feature;
  
          var close = document.createElement('a');
          close.href = '#close';
          close.className = 'close';
          close.innerHTML = 'Close';
          tooltip.appendChild(close);
  
          var closeClick = function(ev) {
              hideTooltip(tooltip);
              _currentTooltip = undefined;
              ev.returnValue = false; // Prevents hash change.
              if (ev.stopPropagation) ev.stopPropagation();
              if (ev.preventDefault) ev.preventDefault();
              return false;
          };
  
          // IE compatibility.
          if (close.addEventListener) {
              close.addEventListener('click', closeClick, false);
          } else if (close.attachEvent) {
              close.attachEvent('onclick', closeClick);
          }
  
          _currentTooltip = tooltip;
      }
  
      t.over = function(feature, context, e) {
          if (!feature) return;
          context.style.cursor = 'pointer';
  
          if (isPopup(_currentTooltip)) {
              return;
          } else {
              _currentTooltip = getTooltip(feature, context);
              moveTooltip(e);
              if (context.addEventListener) {
                  context.addEventListener('mousemove', moveTooltip);
              }
          }
      };
  
      // Hide all tooltips on this layer and show the first hidden tooltip on the
      // highest layer underneath if found.
      t.out = function(context) {
          context.style.cursor = 'default';
  
          if (isPopup(_currentTooltip)) {
              return;
          } else if (_currentTooltip) {
              hideTooltip(_currentTooltip);
              if (context.removeEventListener) {
                  context.removeEventListener('mousemove', moveTooltip);
              }
              _currentTooltip = undefined;
          }
      };
  
      return t;
  };
  // Wax GridUtil
  // ------------
  
  // Wax header
  var wax = wax || {};
  
  // Request
  // -------
  // Request data cache. `callback(data)` where `data` is the response data.
  wax.request = {
      cache: {},
      locks: {},
      promises: {},
      get: function(url, callback) {
          // Cache hit.
          if (this.cache[url]) {
              return callback(this.cache[url][0], this.cache[url][1]);
          // Cache miss.
          } else {
              this.promises[url] = this.promises[url] || [];
              this.promises[url].push(callback);
              // Lock hit.
              if (this.locks[url]) return;
              // Request.
              var that = this;
              this.locks[url] = true;
              reqwest({
                  url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid',
                  type: 'jsonp',
                  jsonpCallback: 'callback',
                  success: function(data) {
                      that.locks[url] = false;
                      that.cache[url] = [null, data];
                      for (var i = 0; i < that.promises[url].length; i++) {
                          that.promises[url][i](that.cache[url][0], that.cache[url][1]);
                      }
                  },
                  error: function(err) {
                      that.locks[url] = false;
                      that.cache[url] = [err, null];
                      for (var i = 0; i < that.promises[url].length; i++) {
                          that.promises[url][i](that.cache[url][0], that.cache[url][1]);
                      }
                  }
              });
          }
      }
  };
  // Templating
  // ---------
  wax.template = function(x) {
      var template = {};
  
      function urlX(url) {
          // Data URIs are subject to a bug in Firefox
          // https://bugzilla.mozilla.org/show_bug.cgi?id=255107
          // which let them be a vector. But WebKit does 'the right thing'
          // or at least 'something' about this situation, so we'll tolerate
          // them.
          if (/^(https?:\/\/|data:image)/.test(url)) {
              return url;
          }
      }
  
      function idX(id) {
          return id;
      }
  
      // Clone the data object such that the '__[format]__' key is only
      // set for this instance of templating.
      template.format = function(options, data) {
          var clone = {};
          for (var key in data) {
              clone[key] = data[key];
          }
          if (options.format) {
              clone['__' + options.format + '__'] = true;
          }
          return html_sanitize(Mustache.to_html(x, clone), urlX, idX);
      };
  
      return template;
  };
  if (!wax) var wax = {};
  
  // A wrapper for reqwest jsonp to easily load TileJSON from a URL.
  wax.tilejson = function(url, callback) {
      reqwest({
          url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid',
          type: 'jsonp',
          jsonpCallback: 'callback',
          success: callback,
          error: callback
      });
  };
  var wax = wax || {};
  wax.tooltip = {};
  
  wax.tooltip = function(options) {
      this._currentTooltip = undefined;
      options = options || {};
      if (options.animationOut) this.animationOut = options.animationOut;
      if (options.animationIn) this.animationIn = options.animationIn;
  };
  
  // Helper function to determine whether a given element is a wax popup.
  wax.tooltip.prototype.isPopup = function(el) {
      return el && el.className.indexOf('wax-popup') !== -1;
  };
  
  // Get the active tooltip for a layer or create a new one if no tooltip exists.
  // Hide any tooltips on layers underneath this one.
  wax.tooltip.prototype.getTooltip = function(feature, context) {
      var tooltip = document.createElement('div'),
      stopPropagation = function (e) { if (e.stopPropagation) { e.stopPropagation(); } else { e.cancelBubble = true; } },
      mouseEvents = ['mousemove', 'mousedown', 'mouseup', 'click'];
  
      tooltip.className = 'wax-tooltip wax-tooltip-0';
      tooltip.innerHTML = feature;
      context.appendChild(tooltip);
  
      // Stop tooltip mouse events bubbling up to the map.
      for (var i in mouseEvents) {
          if (tooltip.addEventListener) {
              tooltip.addEventListener(mouseEvents[i], stopPropagation, false);
          } else if (tooltip.attachEvent) {
              tooltip.attachEvent('on' + mouseEvents[i], stopPropagation);
          }
      }
  
      return tooltip;
  };
  
  // Hide a given tooltip.
  wax.tooltip.prototype.hideTooltip = function(el) {
      if (!el) return;
      var event,
          remove = function() {
          if (this.parentNode) this.parentNode.removeChild(this);
      };
  
      if (el.style['-webkit-transition'] !== undefined && this.animationOut) {
          event = 'webkitTransitionEnd';
      } else if (el.style.MozTransition !== undefined && this.animationOut) {
          event = 'transitionend';
      }
  
      if (event) {
          // This code assumes that transform-supporting browsers
          // also support proper events. IE9 does both.
          el.addEventListener(event, remove, false);
          el.addEventListener('transitionend', remove, false);
          el.className += ' ' + this.animationOut;
      } else {
          if (el.parentNode) el.parentNode.removeChild(el);
      }
  };
  
  // Expand a tooltip to be a "popup". Suspends all other tooltips from being
  // shown until this popup is closed or another popup is opened.
  wax.tooltip.prototype.click = function(feature, context) {
      // Hide any current tooltips.
      if (this._currentTooltip) {
          this.hideTooltip(this._currentTooltip);
          this._currentTooltip = undefined;
      }
  
      var tooltip = this.getTooltip(feature, context);
      tooltip.className += ' wax-popup';
      tooltip.innerHTML = feature;
  
      var close = document.createElement('a');
      close.href = '#close';
      close.className = 'close';
      close.innerHTML = 'Close';
      tooltip.appendChild(close);
  
      var closeClick = wax.util.bind(function(ev) {
          this.hideTooltip(tooltip);
          this._currentTooltip = undefined;
          ev.returnValue = false; // Prevents hash change.
          if (ev.stopPropagation) ev.stopPropagation();
          if (ev.preventDefault) ev.preventDefault();
          return false;
      }, this);
  
      // IE compatibility.
      if (close.addEventListener) {
          close.addEventListener('click', closeClick, false);
          close.addEventListener('touchend', closeClick, false);
      } else if (close.attachEvent) {
          close.attachEvent('onclick', closeClick);
      }
  
      this._currentTooltip = tooltip;
  };
  
  // Show a tooltip.
  wax.tooltip.prototype.over = function(feature, context) {
      if (!feature) return;
      context.style.cursor = 'pointer';
  
      if (this.isPopup(this._currentTooltip)) {
          return;
      } else {
          this._currentTooltip = this.getTooltip(feature, context);
      }
  };
  
  
  // Hide all tooltips on this layer and show the first hidden tooltip on the
  // highest layer underneath if found.
  wax.tooltip.prototype.out = function(context) {
      context.style.cursor = 'default';
  
      if (this.isPopup(this._currentTooltip)) {
          return;
      } else if (this._currentTooltip) {
          this.hideTooltip(this._currentTooltip);
          this._currentTooltip = undefined;
      }
  };
  var wax = wax || {};
  wax.util = wax.util || {};
  
  // Utils are extracted from other libraries or
  // written from scratch to plug holes in browser compatibility.
  wax.util = {
      // From Bonzo
      offset: function(el) {
          // TODO: window margins
          //
          // Okay, so fall back to styles if offsetWidth and height are botched
          // by Firefox.
          var width = el.offsetWidth || parseInt(el.style.width, 10),
              height = el.offsetHeight || parseInt(el.style.height, 10),
              doc_body = document.body,
              top = 0,
              left = 0;
  
          var calculateOffset = function(el) {
              if (el === doc_body || el === document.documentElement) return;
              top += el.offsetTop;
              left += el.offsetLeft;
  
              var style = el.style.transform ||
                  el.style.WebkitTransform ||
                  el.style.OTransform ||
                  el.style.MozTransform ||
                  el.style.msTransform;
  
              if (style) {
                  if (match = style.match(/translate\((.+)px, (.+)px\)/)) {
                      top += parseInt(match[2], 10);
                      left += parseInt(match[1], 10);
                  } else if (match = style.match(/translate3d\((.+)px, (.+)px, (.+)px\)/)) {
                      top += parseInt(match[2], 10);
                      left += parseInt(match[1], 10);
                  } else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) {
                      var pts = match[1].split(',');
                      top += parseInt(pts[13], 10);
                      left += parseInt(pts[12], 10);
                  } else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) {
                      top += parseInt(match[2], 10);
                      left += parseInt(match[1], 10);
                  }
              }
          };
  
          calculateOffset(el);
  
          try {
              while (el = el.offsetParent) calculateOffset(el);
          } catch(e) {
              // Hello, internet explorer.
          }
  
          // Offsets from the body
          top += doc_body.offsetTop;
          left += doc_body.offsetLeft;
          // Offsets from the HTML element
          top += doc_body.parentNode.offsetTop;
          left += doc_body.parentNode.offsetLeft;
  
          // Firefox and other weirdos. Similar technique to jQuery's
          // `doesNotIncludeMarginInBodyOffset`.
          var htmlComputed = document.defaultView ?
              window.getComputedStyle(doc_body.parentNode, null) :
              doc_body.parentNode.currentStyle;
          if (doc_body.parentNode.offsetTop !==
              parseInt(htmlComputed.marginTop, 10) &&
              !isNaN(parseInt(htmlComputed.marginTop, 10))) {
              top += parseInt(htmlComputed.marginTop, 10);
              left += parseInt(htmlComputed.marginLeft, 10);
          }
  
          return {
              top: top,
              left: left,
              height: height,
              width: width
          };
      },
  
      '$': function(x) {
          return (typeof x === 'string') ?
              document.getElementById(x) :
              x;
      },
  
      // From underscore, minus funcbind for now.
      // Returns a version of a function that always has the second parameter,
      // `obj`, as `this`.
      bind: function(func, obj) {
          var args = Array.prototype.slice.call(arguments, 2);
          return function() {
              return func.apply(obj, args.concat(Array.prototype.slice.call(arguments)));
          };
      },
      // From underscore
      isString: function(obj) {
          return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
      },
      // IE doesn't have indexOf
      indexOf: function(array, item) {
          var nativeIndexOf = Array.prototype.indexOf;
          if (array === null) return -1;
          var i, l;
          if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
          for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
          return -1;
      },
      // is this object an array?
      isArray: Array.isArray || function(obj) {
          return Object.prototype.toString.call(obj) === '[object Array]';
      },
      // From underscore: reimplement the ECMA5 `Object.keys()` method
      keys: Object.keys || function(obj) {
          var ho = Object.prototype.hasOwnProperty;
          if (obj !== Object(obj)) throw new TypeError('Invalid object');
          var keys = [];
          for (var key in obj) if (ho.call(obj, key)) keys[keys.length] = key;
          return keys;
      },
      // From quirksmode: normalize the offset of an event from the top-left
      // of the page.
      eventoffset: function(e) {
          var posx = 0;
          var posy = 0;
          if (!e) var e = window.event;
          if (e.pageX || e.pageY) {
              // Good browsers
              return {
                  x: e.pageX,
                  y: e.pageY
              };
          } else if (e.clientX || e.clientY) {
              // Internet Explorer
              var doc = document.documentElement, body = document.body;
              var htmlComputed = document.body.parentNode.currentStyle;
              var topMargin = parseInt(htmlComputed.marginTop, 10) || 0;
              var leftMargin = parseInt(htmlComputed.marginLeft, 10) || 0;
              return {
                  x: e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
                      (doc && doc.clientLeft || body && body.clientLeft || 0) + leftMargin,
                  y: e.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) -
                      (doc && doc.clientTop  || body && body.clientTop  || 0) + topMargin
              };
          } else if (e.touches && e.touches.length === 1) {
              // Touch browsers
              return {
                  x: e.touches[0].pageX,
                  y: e.touches[0].pageY
              };
          }
      },
  
      // Ripped from underscore.js
      // Internal function used to implement `_.throttle` and `_.debounce`.
      limit: function(func, wait, debounce) {
          var timeout;
          return function() {
              var context = this, args = arguments;
              var throttler = function() {
                  timeout = null;
                  func.apply(context, args);
              };
              if (debounce) clearTimeout(timeout);
              if (debounce || !timeout) timeout = setTimeout(throttler, wait);
          };
      },
  
      // Returns a function, that, when invoked, will only be triggered at most once
      // during a given window of time.
      throttle: function(func, wait) {
          return this.limit(func, wait, false);
      }
  };
  // Wax header
  var wax = wax || {};
  wax.ol = wax.ol || {};
  
  var addEv = function(element, name, observer) {
      if (element.addEventListener) {
          element.addEventListener(name, observer, false);
      } else if (element.attachEvent) {
          element.attachEvent('on' + name, observer);
      }
  };
  
  // An interaction toolkit for tiles that implement the
  // [MBTiles UTFGrid spec](https://github.com/mapbox/mbtiles-spec)
  wax.ol.Interaction =
      OpenLayers.Class(OpenLayers.Control, {
      feature: {},
      handlerOptions: null,
      handlers: null,
  
      gm: new wax.GridManager(),
  
      initialize: function(tilejson, options) {
          this.options = options || {};
          this.clickAction = this.options.clickAction || 'full';
          this.gm = new wax.GridManager(tilejson);
  
          OpenLayers.Control.prototype.initialize.apply(this, [this.options || {}]);
  
          this.callbacks = this.options.callbacks || new wax.tooltip();
      },
  
      setMap: function(map) {
          addEv(map.viewPortDiv, 'mousemove', wax.util.bind(this.getInfoForHover, this));
          addEv(map.viewPortDiv, 'mouseout', wax.util.bind(this.resetLayers, this));
          this.clickHandler = new OpenLayers.Handler.Click(
              this, {
                  click: this.getInfoForClick
              }
          );
  
          this.clickHandler.setMap(map);
          this.clickHandler.activate();
  
          map.events.on({
              addlayer: this.resetLayers,
              changelayer: this.resetLayers,
              removelayer: this.resetLayers,
              changebaselayer: this.resetLayers,
              scope: this
          });
  
          OpenLayers.Control.prototype.setMap.apply(this, arguments);
      },
  
      // Get an Array of the stack of tiles under the mouse.
      // This operates with pixels only, since there's no way
      // to bubble through an element which is sitting on the map
      // (like an SVG overlay).
      //
      // If no tiles are under the mouse, returns an empty array.
      getTileStack: function(layers, pos) {
          // If we don't have both an event and some tiles, it's nothing.
          if (!layers || !pos) return [];
          var tiles = [];
          layerfound: for (var j = 0; j < layers.length; j++) {
              for (var x = 0; x < layers[j].grid.length; x++) {
                  for (var y = 0; y < layers[j].grid[x].length; y++) {
                      var divpos;
                      if (layers[j].grid[x][y].imgDiv) {
                          divpos = wax.util.offset(layers[j].grid[x][y].imgDiv);
                      } else {
                          divpos = wax.util.offset(layers[j].grid[x][y].frame);
                      }
                      if (divpos &&
                          ((divpos.top < pos.y) &&
                           ((divpos.top + 256) > pos.y) &&
                           (divpos.left < pos.x) &&
                           ((divpos.left + 256) > pos.x))) {
                          tiles.push(layers[j].grid[x][y]);
                          continue layerfound;
                      }
                  }
              }
          }
          return tiles;
      },
  
      // Get all interactable layers
      viableLayers: function() {
          if (this._viableLayers) return this._viableLayers;
          this._viableLayers = [];
          for (var i in this.map.layers) {
              // TODO: make better indication of whether
              // this is an interactive layer
              if ((this.map.layers[i].visibility === true) &&
                  (this.map.layers[i].CLASS_NAME === 'OpenLayers.Layer.TMS')) {
                this._viableLayers.push(this.map.layers[i]);
              }
          }
          return this._viableLayers;
      },
  
      resetLayers: function(evt) {
          this._viableLayers = null;
          // Fix a condition in which mouseout is called, but the user is really mousing
          // over to a different tile.
          var newTarget = evt.relatedTarget || evt.toElement;
          if (newTarget && newTarget.className !== 'olTileImage') {
              this.callbacks.out(this.map.viewPortDiv);
          }
      },
  
      // React to a click mouse event
      // This is the `pause` handler attached to the map.
      getInfoForClick: function(evt) {
          // If there's no event, this handler should not continue.
          if (!evt) return;
          var layers = this.viableLayers(),
              pos = wax.util.eventoffset(evt),
              tiles = this.getTileStack(this.viableLayers(), pos),
              feature = null,
              g = null;
  
          var that = this;
  
          for (var t = 0; t < tiles.length; t++) {
              if (!tiles[t].url) continue;
              this.gm.getGrid(tiles[t].url, function(err, g) {
                  if (!g) return;
                  var feature = g.tileFeature(pos.x, pos.y, tiles[t].frame || tiles[t].imgDiv, {
                      format: that.clickAction
                  });
                  if (feature) {
                      switch (that.clickAction) {
                          case 'full':
                              that.callbacks.click(feature, tiles[t].layer.map.viewPortDiv, t);
                              break;
                          case 'location':
                              window.location = feature;
                              break;
                      }
                  }
              });
          }
      },
  
      // React to a hover mouse event, by finding all tiles,
      // finding features, and calling `this.callbacks[]`
      // This is the `click` handler attached to the map.
      getInfoForHover: function(evt) {
          // If there's no event, this handler should not proceed.
          if (!evt) return;
          var options = { format: 'teaser' },
              layers = this.viableLayers(),
              pos = wax.util.eventoffset(evt),
              tiles = this.getTileStack(this.viableLayers(), pos),
              feature = null,
              g = null;
  
          var that = this;
  
          for (var t = 0; t < tiles.length; t++) {
              if (!tiles[t].url) continue;
              // This features has already been loaded, or
              // is currently being requested.
              this.gm.getGrid(tiles[t].url, function(err, g) {
                  if (g && tiles[t]) {
                      var feature = g.tileFeature(pos.x, pos.y, tiles[t].frame || tiles[t].imgDiv, options);
  
                      if (feature) {
                          if (!tiles[t]) return;
                          if (feature && that.feature[t] !== feature) {
                              that.feature[t] = feature;
                              that.callbacks.out(tiles[t].layer.map.div);
                              that.callbacks.over(feature, tiles[t].layer.map.div, evt);
                          } else if (!feature) {
                              that.feature[t] = null;
                              that.callbacks.out(tiles[t].layer.map.div);
                          }
                      } else {
                          // Request this feature
                          // TODO(tmcw) re-add layer
                          // Only nix this tooltip if the current tooltip is
                          // owned by this layer
                          if (that.feature[t]) {
                              that.callbacks.out(tiles[t].layer.map.div);
                          }
                          that.feature[t] = null;
                      }
                  }
              });
          }
      },
      CLASS_NAME: 'wax.ol.Interaction'
  });
  // Wax: Legend Control
  // -------------------
  
  // Wax header
  var wax = wax || {};
  wax.ol = wax.ol || {};
  
  wax.ol.Legend = OpenLayers.Class(OpenLayers.Control, {
      CLASS_NAME: 'wax.ol.Legend',
      legend: null,
      options: null,
  
      initialize: function(options) {
          this.options = options || {};
          OpenLayers.Control.prototype.initialize.apply(this, [options || {}]);
      },
  
      activate: function() {
          this.legend = new wax.Legend(this.map.viewPortDiv, this.options.container);
          return OpenLayers.Control.prototype.activate.apply(this, arguments);
      },
  
      setMap: function(map) {
          OpenLayers.Control.prototype.setMap.apply(this, arguments);
          this.activate();
          this.map.events.on({
              'addlayer': this.setLegend,
              'changelayer': this.setLegend,
              'removelayer': this.setLegend,
              'changebaselayer': this.setLegend,
              scope: this
          });
      },
  
      setLegend: function() {
          var urls = [];
          for (var i = 0; i < this.map.layers.length; i++) {
              var layer = this.map.layers[i];
              if (layer && layer.getURL && layer.visibility) {
                  urls.push(layer.getURL(new OpenLayers.Bounds()));
              }
          }
          this.legend.render(urls);
      }
  });
  