/* wax - 4.1.3 - 1.0.4-477-gba3b176 */ /*! * 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 * 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: π is different from Π 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 < 2 && 4 > 3 ') * # '1 < 2 && 4 > 3\n' * $ unescapeEntities('<< <- unfinished entity>') * # '<< <- unfinished entity>' * $ unescapeEntities('/foo?bar=baz©=true') // & often unescaped in URLS * # '/foo?bar=baz©=true' * $ unescapeEntities('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 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. * # '"<<&==&>>"' * $ escapeAttrib('Hello !') * # 'Hello <World>!' * } */ function escapeAttrib(s) { // Escaping '=' defangs many UTF-7 and SGML short-tag attacks. return s.replace(ampRe, '&').replace(ltRe, '<').replace(gtRe, '>') .replace(quotRe, '"').replace(eqRe, '='); } /** * Escape entities in RCDATA that can be escaped without changing the meaning. * {@updoc * $ normalizeRCData('1 < 2 && 3 > 4 && 5 < 7&8') * # '1 < 2 && 3 > 4 && 5 < 7&8' * } */ function normalizeRCData(rcdata) { return rcdata .replace(looseAmpRe, '&$1') .replace(ltRe, '<') .replace(gtRe, '>'); } // 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 // , 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]*?--\>|]*>|<\\?[^>*]*>' // '/' 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 // // 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('' ? '>' : '&', 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(''); } } stack.length = index; out.push(''); } }, 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.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 "&"; case '"': return '"'; case "'": return '''; case "<": return "<"; case ">": return ">"; 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"); } } }); }(); ;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 section of the mbtiles spec](https://github.com/mapbox/mbtiles-spec/blob/master/1.1/utfgrid.md) // 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'); tooltip.className = 'wax-tooltip wax-tooltip-0'; tooltip.innerHTML = feature; context.appendChild(tooltip); 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); } 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); }, // parseUri 1.2.2 // Steven Levithan parseUri: function(str) { var o = { strictMode: false, key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], q: { name: "queryKey", parser: /(?:^|&)([^&=]*)=?([^&]*)/g }, parser: { strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ } }, m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), uri = {}, i = 14; while (i--) uri[o.key[i]] = m[i] || ""; uri[o.q.name] = {}; uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { if ($1) uri[o.q.name][$1] = $2; }); return uri; }, // appends callback onto urls regardless of existing query params addUrlData: function(url, data) { url += (this.parseUri(url).query) ? '&' : '?'; return url += data; } }; wax = wax || {}; wax.g = wax.g || {}; // Attribution // ----------- // Attribution wrapper for Google Maps. wax.g.attribution = function(map, tilejson) { tilejson = tilejson || {}; var a, // internal attribution control attribution = {}; attribution.element = function() { return a.element(); }; attribution.appendTo = function(elem) { wax.util.$(elem).appendChild(a.element()); return this; }; attribution.init = function() { a = wax.attribution(); a.set(tilejson.attribution); a.element().className = 'wax-attribution wax-g'; return this; }; return attribution.init(); }; wax = wax || {}; wax.g = wax.g || {}; // Bandwidth Detection // ------------------ wax.g.bwdetect = function(map, options) { options = options || {}; var lowpng = options.png || '.png128', lowjpg = options.jpg || '.jpg70'; // Create a low-bandwidth map type. if (!map.mapTypes['mb-low']) { var mb = map.mapTypes.mb; var tilejson = { tiles: [], scheme: mb.options.scheme, blankImage: mb.options.blankImage, minzoom: mb.minZoom, maxzoom: mb.maxZoom, name: mb.name, description: mb.description }; for (var i = 0; i < mb.options.tiles.length; i++) { tilejson.tiles.push(mb.options.tiles[i] .replace('.png', lowpng) .replace('.jpg', lowjpg)); } m.mapTypes.set('mb-low', new wax.g.connector(tilejson)); } return wax.bwdetect(options, function(bw) { map.setMapTypeId(bw ? 'mb' : 'mb-low'); }); }; wax = wax || {}; wax.g = wax.g || {}; wax.g.hash = function(map) { return wax.hash({ getCenterZoom: function() { var center = map.getCenter(), zoom = map.getZoom(), precision = Math.max( 0, Math.ceil(Math.log(zoom) / Math.LN2)); return [zoom.toFixed(2), center.lat().toFixed(precision), center.lng().toFixed(precision) ].join('/'); }, setCenterZoom: function setCenterZoom(args) { map.setCenter(new google.maps.LatLng(args[1], args[2])); map.setZoom(args[0]); }, bindChange: function(fn) { google.maps.event.addListener(map, 'idle', fn); }, unbindChange: function(fn) { google.maps.event.removeListener(map, 'idle', fn); } }); }; wax = wax || {}; wax.g = wax.g || {}; // A control that adds interaction to a google Map object. // // Takes an options object with the following keys: // // * `callbacks` (optional): an `out`, `over`, and `click` callback. // If not given, the `wax.tooltip` library will be expected. // * `clickAction` (optional): **full** or **location**: default is // **full**. wax.g.interaction = function(map, tilejson, options) { tilejson = tilejson || {}; options = options || {}; // Our GridManager (from `gridutil.js`). This will keep the // cache of grid information and provide friendly utility methods // that return `GridTile` objects instead of raw data. var interaction = { waxGM: new wax.GridManager(tilejson), // This requires wax.Tooltip or similar callbacks: options.callbacks || new wax.tooltip(), clickAction: options.clickAction || 'full', eventHandlers:{}, // Attach listeners to the map add: function() { this.eventHandlers.tileloaded = google.maps.event.addListener(map, 'tileloaded', wax.util.bind(this.clearTileGrid, this)); this.eventHandlers.idle = google.maps.event.addListener(map, 'idle', wax.util.bind(this.clearTileGrid, this)); this.eventHandlers.mousemove = google.maps.event.addListener(map, 'mousemove', this.onMove()); this.eventHandlers.click = google.maps.event.addListener(map, 'click', this.click()); return this; }, // Remove interaction events from the map. remove: function() { google.maps.event.removeListener(this.eventHandlers.tileloaded); google.maps.event.removeListener(this.eventHandlers.idle); google.maps.event.removeListener(this.eventHandlers.mousemove); google.maps.event.removeListener(this.eventHandlers.click); return this; }, // Search through `.tiles` and determine the position, // from the top-left of the **document**, and cache that data // so that `mousemove` events don't always recalculate. getTileGrid: function() { // Get all 'marked' tiles, added by the `wax.g.MapType` layer. // Return an array of objects which have the **relative** offset of // each tile, with a reference to the tile object in `tile`, since the API // returns evt coordinates as relative to the map object. if (!this._getTileGrid) { this._getTileGrid = []; var zoom = map.getZoom(); var mapOffset = wax.util.offset(map.getDiv()); var get = wax.util.bind(function(mapType) { if (!mapType.interactive) return; for (var key in mapType.cache) { if (key.split('/')[0] != zoom) continue; var tileOffset = wax.util.offset(mapType.cache[key]); this._getTileGrid.push([ tileOffset.top - mapOffset.top, tileOffset.left - mapOffset.left, mapType.cache[key] ]); } }, this); // Iterate over base mapTypes and overlayMapTypes. for (var i in map.mapTypes) get(map.mapTypes[i]); map.overlayMapTypes.forEach(get); } return this._getTileGrid; }, clearTileGrid: function(map, e) { this._getTileGrid = null; }, getTile: function(evt) { var tile; var grid = this.getTileGrid(); for (var i = 0; i < grid.length; i++) { if ((grid[i][0] < evt.pixel.y) && ((grid[i][0] + 256) > evt.pixel.y) && (grid[i][1] < evt.pixel.x) && ((grid[i][1] + 256) > evt.pixel.x)) { tile = grid[i][2]; break; } } return tile || false; }, onMove: function(evt) { if (!this._onMove) this._onMove = wax.util.bind(function(evt) { var tile = this.getTile(evt); if (tile) { this.waxGM.getGrid(tile.src, wax.util.bind(function(err, g) { if (err || !g) return; var feature = g.tileFeature( evt.pixel.x + wax.util.offset(map.getDiv()).left, evt.pixel.y + wax.util.offset(map.getDiv()).top, tile, { format: 'teaser' } ); // Support only a single layer. // Thus a layer index of **0** is given to the tooltip library if (feature && this.feature !== feature) { this.feature = feature; this.callbacks.out(map.getDiv()); this.callbacks.over(feature, map.getDiv(), 0, evt); } else if (!feature) { this.feature = null; this.callbacks.out(map.getDiv()); } }, this)); } }, this); return this._onMove; }, click: function(evt) { if (!this._onClick) this._onClick = wax.util.bind(function(evt) { // Feature previously found? Don't continue with the event... // @vizzuality change! if (window.event && window.event.cancelBubble) { return false; } var tile = this.getTile(evt); if (tile) { this.waxGM.getGrid(tile.src, wax.util.bind(function(err, g) { if (err || !g) return; var feature = g.tileFeature( evt.pixel.x + wax.util.offset(map.getDiv()).left, evt.pixel.y + wax.util.offset(map.getDiv()).top, tile, { format: this.clickAction } ); if (feature) { // Stop propagation of the click event to avoid fire more wax clicks!!! // @vizzuality change! // For Firefox browser that doesn't recognize window.event if (!window.event) { window.event = {}; setTimeout(function(){ window.event = null; },150); } window.event.cancelBubble = true; switch (this.clickAction) { case 'full': this.callbacks.click(feature, map.getDiv(), 0, evt); break; case 'location': window.location = feature; break; } } }, this)); } }, this); return this._onClick; } }; // Return the interaction control such that the caller may manipulate it // e.g. remove it. return interaction.add(map); }; wax = wax || {}; wax.g = wax.g || {}; // Legend Control // -------------- // Adds legends to a google Map object. wax.g.legend = function(map, tilejson) { tilejson = tilejson || {}; var l, // parent legend legend = {}; legend.add = function() { l = wax.legend() .content(tilejson.legend || ''); return this; }; legend.element = function() { return l.element(); }; legend.appendTo = function(elem) { wax.util.$(elem).appendChild(l.element()); return this; }; return legend.add(); }; // Wax for Google Maps API v3 // -------------------------- // Wax header var wax = wax || {}; wax.g = wax.g || {}; // Wax Google Maps MapType: takes an object of options in the form // // { // name: '', // filetype: '.png', // layerName: 'world-light', // alt: '', // zoomRange: [0, 18], // baseUrl: 'a url', // } wax.g.connector = function(options) { options = options || {}; this.options = { tiles: options.tiles, scheme: options.scheme || 'xyz', blankImage: options.blankImage }; this.minZoom = options.minzoom || 0; this.maxZoom = options.maxzoom || 22; this.name = options.name || ''; this.description = options.description || ''; // non-configurable options this.interactive = true; this.tileSize = new google.maps.Size(256, 256); // DOM element cache this.cache = {}; }; // Get a tile element from a coordinate, zoom level, and an ownerDocument. wax.g.connector.prototype.getTile = function(coord, zoom, ownerDocument) { var key = zoom + '/' + coord.x + '/' + coord.y; if (!this.cache[key]) { var img = this.cache[key] = new Image(256, 256); this.cache[key].src = this.getTileUrl(coord, zoom); this.cache[key].setAttribute('gTileKey', key); this.cache[key].onerror = function() { img.style.display = 'none'; }; } return this.cache[key]; }; // Remove a tile that has fallen out of the map's viewport. // // TODO: expire cache data in the gridmanager. wax.g.connector.prototype.releaseTile = function(tile) { var key = tile.getAttribute('gTileKey'); this.cache[key] && delete this.cache[key]; tile.parentNode && tile.parentNode.removeChild(tile); }; // Get a tile url, based on x, y coordinates and a z value. wax.g.connector.prototype.getTileUrl = function(coord, z) { // Y coordinate is flipped in Mapbox, compared to Google var mod = Math.pow(2, z), y = (this.options.scheme === 'tms') ? (mod - 1) - coord.y : coord.y, x = (coord.x % mod); x = (x < 0) ? (coord.x % mod) + mod : x; if (y < 0) return this.options.blankImage; return this.options.tiles [parseInt(x + y, 10) % this.options.tiles.length] .replace('{z}', z) .replace('{x}', x) .replace('{y}', y); };