diff --git a/examples/tilejson.html b/examples/tilejson.html index 6c6c6a60..9866c2f2 100644 --- a/examples/tilejson.html +++ b/examples/tilejson.html @@ -1,16 +1,32 @@ + +
+
+ - + + + + + - diff --git a/examples/vendor/backbone.js b/examples/vendor/backbone.js new file mode 100644 index 00000000..c9249656 --- /dev/null +++ b/examples/vendor/backbone.js @@ -0,0 +1,1894 @@ +// Backbone.js 1.2.3 + +// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(factory) { + + // Establish the root object, `window` (`self`) in the browser, or `global` on the server. + // We use `self` instead of `window` for `WebWorker` support. + var root = (typeof self == 'object' && self.self == self && self) || + (typeof global == 'object' && global.global == global && global); + + // Set up Backbone appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + + // Next for Node.js or CommonJS. jQuery may not be needed as a module. + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'), $; + try { $ = require('jquery'); } catch(e) {} + factory(root, exports, _, $); + + // Finally, as a browser global. + } else { + root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); + } + +}(function(root, Backbone, _, $) { + + // Initial Setup + // ------------- + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to a common array method we'll want to use later. + var slice = Array.prototype.slice; + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.2.3'; + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = $; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... this will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Proxy Backbone class methods to Underscore functions, wrapping the model's + // `attributes` object or collection's `models` array behind the scenes. + // + // collection.filter(function(model) { return model.get('age') > 10 }); + // collection.each(this.addView); + // + // `Function#apply` can be slow so we use the method's arg count, if we know it. + var addMethod = function(length, method, attribute) { + switch (length) { + case 1: return function() { + return _[method](this[attribute]); + }; + case 2: return function(value) { + return _[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return _[method](this[attribute], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return _[method](this[attribute], cb(iteratee, this), defaultVal, context); + }; + default: return function() { + var args = slice.call(arguments); + args.unshift(this[attribute]); + return _[method].apply(_, args); + }; + } + }; + var addUnderscoreMethods = function(Class, methods, attribute) { + _.each(methods, function(length, method) { + if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); + }); + }; + + // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. + var cb = function(iteratee, instance) { + if (_.isFunction(iteratee)) return iteratee; + if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; + }; + var modelMatcher = function(attrs) { + var matcher = _.matches(attrs); + return function(model) { + return matcher(model.attributes); + }; + }; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // a custom event channel. You may bind a callback to an event with `on` or + // remove with `off`; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = {}; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Iterates over the standard `event, callback` (as well as the fancy multiple + // space-separated events `"change blur", callback` and jQuery-style event + // maps `{event: callback}`). + var eventsApi = function(iteratee, events, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; + for (names = _.keys(name); i < names.length ; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); + } + } else if (name && eventSplitter.test(name)) { + // Handle space separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); + } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; + }; + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + Events.on = function(name, callback, context) { + return internalOn(this, name, callback, context); + }; + + // Guard the `listening` argument from the public API. + var internalOn = function(obj, name, callback, context, listening) { + obj._events = eventsApi(onApi, obj._events || {}, name, callback, { + context: context, + ctx: obj, + listening: listening + }); + + if (listening) { + var listeners = obj._listeners || (obj._listeners = {}); + listeners[listening.id] = listening; + } + + return obj; + }; + + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + var thisId = this._listenId || (this._listenId = _.uniqueId('l')); + listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; + } + + // Bind callbacks on obj, and keep track of them on listening. + internalOn(obj, name, callback, this, listening); + return this; + }; + + // The reducing API that adds a callback to the `events` object. + var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); + } + return events; + }; + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + return this; + }; + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : _.keys(listeningTo); + + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + } + if (_.isEmpty(listeningTo)) this._listeningTo = void 0; + + return this; + }; + + // The reducing API that removes a callback from the `events` object. + var offApi = function(events, name, callback, options) { + if (!events) return; + + var i = 0, listening; + var context = options.context, listeners = options.listeners; + + // Delete all events listeners and "drop" events. + if (!name && !callback && !context) { + var ids = _.keys(listeners); + for (; i < ids.length; i++) { + listening = listeners[ids[i]]; + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; + } + return; + } + + var names = name ? [name] : _.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Replace events if there are any remaining. Otherwise, clean up. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + listening = handler.listening; + if (listening && --listening.count === 0) { + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; + } + } + } + + // Update tail event if the list has any events. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; + } + } + if (_.size(events)) return events; + }; + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + Events.once = function(name, callback, context) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); + return this.on(events, void 0, context); + }; + + // Inversion-of-control versions of `once`. + Events.listenToOnce = function(obj, name, callback) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); + return this.listenTo(obj, events); + }; + + // Reduces the event callbacks into a map of `{event: onceWrapper}`. + // `offer` unbinds the `onceWrapper` after it has been called. + var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = _.once(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; + }; + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; + }; + + // Handles triggering the appropriate event callbacks. + var triggerApi = function(objEvents, name, cb, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + } + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId(this.cidPrefix); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + attrs = _.defaults({}, attrs, _.result(this, 'defaults')); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Special-cased proxy to underscore's `_.matches` method. + matches: function(attrs) { + return !!_.iteratee(attrs, this)(this.attributes); + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + var unset = options.unset; + var silent = options.silent; + var changes = []; + var changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; + + // For each `set` attribute, update or delete the current value. + for (var attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + changed[attr] = val; + } else { + delete changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Update the `id`. + this.id = this.get(this.idAttribute); + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = options; + for (var i = 0; i < changes.length; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + options = this._pending; + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var old = this._changing ? this._previousAttributes : this.attributes; + var changed = {}; + for (var attr in diff) { + var val = diff[attr]; + if (_.isEqual(old[attr], val)) continue; + changed[attr] = val; + } + return _.size(changed) ? changed : false; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server, merging the response with the model's + // local attributes. Any changed attributes will trigger a "change" event. + fetch: function(options) { + options = _.extend({parse: true}, options); + var model = this; + var success = options.success; + options.success = function(resp) { + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (!model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true, parse: true}, options); + var wait = options.wait; + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !wait) { + if (!this.set(attrs, options)) return false; + } else { + if (!this._validate(attrs, options)) return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + var attributes = this.attributes; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (serverAttrs && !model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + // Set temporary attributes if `{wait: true}` to properly find new ids. + if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); + + var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch' && !options.attrs) options.attrs = attrs; + var xhr = this.sync(method, this, options); + + // Restore attributes. + this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + var wait = options.wait; + + var destroy = function() { + model.stopListening(); + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (wait) destroy(); + if (success) success.call(options.context, model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + var xhr = false; + if (this.isNew()) { + _.defer(options.success); + } else { + wrapError(this, options); + xhr = this.sync('delete', this, options); + } + if (!wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = + _.result(this, 'urlRoot') || + _.result(this.collection, 'url') || + urlError(); + if (this.isNew()) return base; + var id = this.get(this.idAttribute); + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return !this.has(this.idAttribute); + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.defaults({validate: true}, options)); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model, mapped to the + // number of arguments they take. + var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, + omit: 0, chain: 1, isEmpty: 1 }; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + addUnderscoreMethods(Model, modelMethods, 'attributes'); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analogous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Splices `insert` into `array` at index `at`. + var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + for (var i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model) { return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. `models` may be Backbone + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + options = _.extend({}, options); + var singular = !_.isArray(models); + models = singular ? [models] : _.clone(models); + var removed = this._removeModels(models, options); + if (!options.silent && removed) this.trigger('update', this, options); + return singular ? removed[0] : removed; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + if (models == null) return; + + options = _.defaults({}, options, setOptions); + if (options.parse && !this._isModel(models)) models = this.parse(models, options); + + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + + var at = options.at; + if (at != null) at = +at; + if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + var model; + for (var i = 0; i < models.length; i++) { + model = models[i]; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } + } + } + + // Remove stale models. + if (remove) { + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); + } + if (toRemove.length) this._removeModels(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length != set.length || _.some(this.models, function(model, index) { + return model !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.length) { + if (sortable) sort = true; + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort events. + if (!options.silent) { + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); + } + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length) this.trigger('update', this, options); + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options = options ? _.clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { + this._removeReference(this.models[i], options); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + return this.remove(model, options); + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + return this.remove(model, options); + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + var id = this.modelId(this._isModel(obj) ? obj.attributes : obj); + return this._byId[obj] || this._byId[id] || this._byId[obj.cid]; + }, + + // Get the model at the given index. + at: function(index) { + if (index < 0) index += this.length; + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + return this[first ? 'find' : 'filter'](attrs); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + var length = comparator.length; + if (_.isFunction(comparator)) comparator = _.bind(comparator, this); + + // Run sort based on type of `comparator`. + if (length === 1 || _.isString(comparator)) { + this.models = this.sortBy(comparator); + } else { + this.models.sort(comparator); + } + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = _.extend({parse: true}, options); + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success.call(options.context, collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + var wait = options.wait; + model = this._prepareModel(model, options); + if (!model) return false; + if (!wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(model, resp, callbackOpts) { + if (wait) collection.add(model, callbackOpts); + if (success) success.call(callbackOpts.context, model, resp, callbackOpts); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function (attrs) { + return attrs[this.model.prototype.idAttribute || 'id']; + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method called by both remove and set. + _removeModels: function(models, options) { + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = this.get(models[i]); + if (!model) continue; + + var index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + + removed.push(model); + this._removeReference(model, options); + } + return removed.length ? removed : false; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function (model) { + return model instanceof Model; + }, + + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + var id = this.modelId(model.attributes); + if (id != null) this._byId[id] = model; + model.on('all', this._onModelEvent, this); + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (event === 'change') { + var prevId = this.modelId(model.previousAttributes()); + var id = this.modelId(model.attributes); + if (prevId !== id) { + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4, + foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, + contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, + head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, + without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, + isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, + sortBy: 3, indexBy: 3}; + + // Mix in each Underscore method as a proxy to `Collection#models`. + addUnderscoreMethods(Collection, collectionMethods, 'models'); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be set as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this._removeElement(); + this.stopListening(); + return this; + }, + + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); + return this; + }, + + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + delegateEvents: function(events) { + events || (events = _.result(this, 'events')); + if (!events) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[method]; + if (!method) continue; + var match = key.match(delegateEventSplitter); + this.delegate(match[1], match[2], _.bind(method, this)); + } + return this; + }, + + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Clears all callbacks previously bound to the view by `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + this.setElement(this._createElement(_.result(this, 'tagName'))); + this._setAttributes(attrs); + } else { + this.setElement(_.result(this, 'el')); + } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // Pass along `textStatus` and `errorThrown` from jQuery. + var error = options.error; + options.error = function(xhr, textStatus, errorThrown) { + options.textStatus = textStatus; + options.errorThrown = errorThrown; + if (error) error.call(options.context, xhr, textStatus, errorThrown); + }; + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + if (router.execute(callback, args, name) !== false) { + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + } + }); + return this; + }, + + // Execute a route handler with the provided parameters. This is an + // excellent place to do pre-route setup or post-route cleanup. + execute: function(callback, args, name) { + if (callback) callback.apply(this, args); + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param, i) { + // Don't decode the search params. + if (i === params.length - 1) return param || null; + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + this.checkUrl = _.bind(this.checkUrl, this); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for stripping urls of hash. + var pathStripper = /#.*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Are we at the app root? + atRoot: function() { + var path = this.location.pathname.replace(/[^\/]$/, '$&/'); + return path === this.root && !this.getSearch(); + }, + + // Does the pathname match the root? + matchRoot: function() { + var path = this.decodeFragment(this.location.pathname); + var root = path.slice(0, this.root.length - 1) + '/'; + return root === this.root; + }, + + // Unicode characters in `location.pathname` are percent encoded so they're + // decoded for comparison. `%25` should not be decoded since it may be part + // of an encoded parameter. + decodeFragment: function(fragment) { + return decodeURI(fragment.replace(/%25/g, '%2525')); + }, + + // In IE6, the hash fragment and search params are incorrect if the + // fragment contains `?`. + getSearch: function() { + var match = this.location.href.replace(/#.*/, '').match(/\?.+/); + return match ? match[0] : ''; + }, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the pathname and search params, without the root. + getPath: function() { + var path = this.decodeFragment( + this.location.pathname + this.getSearch() + ).slice(this.root.length - 1); + return path.charAt(0) === '/' ? path.slice(1) : path; + }, + + // Get the cross-browser normalized URL fragment from the path or hash. + getFragment: function(fragment) { + if (fragment == null) { + if (this._usePushState || !this._wantsHashChange) { + fragment = this.getPath(); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error('Backbone.history has already been started'); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); + this._useHashChange = this._wantsHashChange && this._hasHashChange; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.history && this.history.pushState); + this._usePushState = this._wantsPushState && this._hasPushState; + this.fragment = this.getFragment(); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + // Transition from hashChange to pushState or vice versa if both are + // requested. + if (this._wantsHashChange && this._wantsPushState) { + + // If we've started off with a route from a `pushState`-enabled + // browser, but we're currently in a browser that doesn't support it... + if (!this._hasPushState && !this.atRoot()) { + var root = this.root.slice(0, -1) || '/'; + this.location.replace(root + '#' + this.getPath()); + // Return immediately as browser will do redirect to new url + return true; + + // Or if we've started out with a hash-based route, but we're currently + // in a browser where it could be `pushState`-based instead... + } else if (this._hasPushState && this.atRoot()) { + this.navigate(this.getHash(), {replace: true}); + } + + } + + // Proxy an iframe to handle location events if the browser doesn't + // support the `hashchange` event, HTML5 history, or the user wants + // `hashChange` but not `pushState`. + if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { + this.iframe = document.createElement('iframe'); + this.iframe.src = 'javascript:0'; + this.iframe.style.display = 'none'; + this.iframe.tabIndex = -1; + var body = document.body; + // Using `appendChild` will throw on IE < 9 if the document is not ready. + var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; + iWindow.document.open(); + iWindow.document.close(); + iWindow.location.hash = '#' + this.fragment; + } + + // Add a cross-platform `addEventListener` shim for older browsers. + var addEventListener = window.addEventListener || function (eventName, listener) { + return attachEvent('on' + eventName, listener); + }; + + // Depending on whether we're using pushState or hashes, and whether + // 'onhashchange' is supported, determine how we check the URL state. + if (this._usePushState) { + addEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + addEventListener('hashchange', this.checkUrl, false); + } else if (this._wantsHashChange) { + this._checkUrlInterval = setInterval(this.checkUrl, this.interval); + } + + if (!this.options.silent) return this.loadUrl(); + }, + + // Disable Backbone.history, perhaps temporarily. Not useful in a real app, + // but possibly useful for unit testing Routers. + stop: function() { + // Add a cross-platform `removeEventListener` shim for older browsers. + var removeEventListener = window.removeEventListener || function (eventName, listener) { + return detachEvent('on' + eventName, listener); + }; + + // Remove window listeners. + if (this._usePushState) { + removeEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + removeEventListener('hashchange', this.checkUrl, false); + } + + // Clean up the iframe if necessary. + if (this.iframe) { + document.body.removeChild(this.iframe); + this.iframe = null; + } + + // Some environments will throw when clearing an undefined interval. + if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); + History.started = false; + }, + + // Add a route to be tested when the fragment changes. Routes added later + // may override previous routes. + route: function(route, callback) { + this.handlers.unshift({route: route, callback: callback}); + }, + + // Checks the current URL to see if it has changed, and if it has, + // calls `loadUrl`, normalizing across the hidden iframe. + checkUrl: function(e) { + var current = this.getFragment(); + + // If the user pressed the back button, the iframe's hash will have + // changed and we should use that for comparison. + if (current === this.fragment && this.iframe) { + current = this.getHash(this.iframe.contentWindow); + } + + if (current === this.fragment) return false; + if (this.iframe) this.navigate(current); + this.loadUrl(); + }, + + // Attempt to load the current URL fragment. If a route succeeds with a + // match, returns `true`. If no defined routes matches the fragment, + // returns `false`. + loadUrl: function(fragment) { + // If the root doesn't match, no routes can match either. + if (!this.matchRoot()) return false; + fragment = this.fragment = this.getFragment(fragment); + return _.some(this.handlers, function(handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + }, + + // Save a fragment into the hash history, or replace the URL state if the + // 'replace' option is passed. You are responsible for properly URL-encoding + // the fragment in advance. + // + // The options object can contain `trigger: true` if you wish to have the + // route callback be fired (not usually desirable), or `replace: true`, if + // you wish to modify the current URL without adding an entry to the history. + navigate: function(fragment, options) { + if (!History.started) return false; + if (!options || options === true) options = {trigger: !!options}; + + // Normalize the fragment. + fragment = this.getFragment(fragment || ''); + + // Don't include a trailing slash on the root. + var root = this.root; + if (fragment === '' || fragment.charAt(0) === '?') { + root = root.slice(0, -1) || '/'; + } + var url = root + fragment; + + // Strip the hash and decode for matching. + fragment = this.decodeFragment(fragment.replace(pathStripper, '')); + + if (this.fragment === fragment) return; + this.fragment = fragment; + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._usePushState) { + this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this._updateHash(this.location, fragment, options.replace); + if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) { + var iWindow = this.iframe.contentWindow; + + // Opening and closing the iframe tricks IE7 and earlier to push a + // history entry on hash-tag change. When replace is true, we don't + // want this. + if (!options.replace) { + iWindow.document.open(); + iWindow.document.close(); + } + + this._updateHash(iWindow.location, fragment, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + return this.location.assign(url); + } + if (options.trigger) return this.loadUrl(fragment); + }, + + // Update the hash location, either replacing the current entry, or adding + // a new one to the browser history. + _updateHash: function(location, fragment, replace) { + if (replace) { + var href = location.href.replace(/(javascript:|#).*$/, ''); + location.replace(href + '#' + fragment); + } else { + // Some browsers require that `hash` contains a leading #. + location.hash = '#' + fragment; + } + } + + }); + + // Create the default Backbone.history. + Backbone.history = new History; + + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var extend = function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && _.has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + _.extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent` constructor function. + var Surrogate = function(){ this.constructor = child; }; + Surrogate.prototype = parent.prototype; + child.prototype = new Surrogate; + + // Add prototype properties (instance properties) to the subclass, + // if supplied. + if (protoProps) _.extend(child.prototype, protoProps); + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; + }; + + // Set up inheritance for the model, collection, router, view and history. + Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; + + // Throw an error when a URL is needed, and none is supplied. + var urlError = function() { + throw new Error('A "url" property or function must be specified'); + }; + + // Wrap an optional error callback with a fallback error event. + var wrapError = function(model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error.call(options.context, model, resp, options); + model.trigger('error', model, resp, options); + }; + }; + + return Backbone; + +})); diff --git a/examples/vendor/cartodb.css b/examples/vendor/cartodb.css new file mode 100644 index 00000000..75e8d74b --- /dev/null +++ b/examples/vendor/cartodb.css @@ -0,0 +1,2 @@ +/* CartoDB.css minified version: 3.15.8 */ +a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}table{border-collapse:collapse;border-spacing:0}button{margin:0;padding:0;border:0;background:0 0;box-sizing:border-box;outline:0}dd,dt{display:inline-block}div.cartodb-popup.dark .jspContainer:after{background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,transparent),color-stop(100%,#000));background:-webkit-linear-gradient(top,transparent,#000);background:-moz-linear-gradient(top,transparent,#000);background:-o-linear-gradient(top,transparent,#000);background:linear-gradient(top,transparent,#000)}div.cartodb-popup.dark .jspContainer:before{background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#000),color-stop(100%,transparent));background:-webkit-linear-gradient(top,#000,transparent);background:-moz-linear-gradient(top,#000,transparent);background:-o-linear-gradient(top,#000,transparent);background:linear-gradient(top,#000,transparent)}div.cartodb-popup.dark{background:url(../img/dark.png) no-repeat -226px 0}div.cartodb-popup.dark div.cartodb-popup-content-wrapper{background:url(../img/dark.png) repeat-y -452px 0}div.cartodb-popup.dark div.cartodb-popup-tip-container{background:url(../img/dark.png) no-repeat 0 0}div.cartodb-popup.dark a.cartodb-popup-close-button{background:url(../img/dark.png) no-repeat 0 -23px}div.cartodb-popup.dark h4{color:#999}div.cartodb-popup.dark p{color:#FFF}div.cartodb-popup.dark a{color:#397DB9}div.cartodb-popup.dark p.empty{font-style:italic;color:#AAA}div.cartodb-popup.dark .jspDrag{background:#AAA;background:rgba(255,255,255,.5)}div.cartodb-popup.dark .jspDrag:hover{background:#DEDEDE;background:rgba(255,255,255,.8)}div.cartodb-popup.v2.dark{background:#000}div.cartodb-popup.v2.dark div.cartodb-popup-tip-container:after,div.cartodb-popup.v2.dark:before{border-top-color:#000}div.cartodb-popup.v2.dark a.cartodb-popup-close-button{background:#000}div.cartodb-popup.v2.dark a.cartodb-popup-close-button:after,div.cartodb-popup.v2.dark a.cartodb-popup-close-button:before{background:#fff}@media \0screen\,screen\9{div.cartodb-popup.v2.dark{border:4px solid #AAA}div.cartodb-popup.v2.dark div.cartodb-popup-tip-container{border-top:18px solid #000}div.cartodb-popup.v2.dark a.cartodb-popup-close-button{border:2px solid #AAA;color:#fff}div.cartodb-popup.v2.dark a.cartodb-popup-close-button:hover{border:2px solid #BBB}}div.cartodb-infowindow{position:absolute;z-index:12}div.cartodb-popup{position:relative;width:226px;height:auto;padding:7px 0 0;margin:0;background:url(../img/light.png) no-repeat -226px 0}div.cartodb-popup div.cartodb-popup-content-wrapper{width:190px;max-width:190px;padding:12px 19px;overflow-x:hidden;background:url(../img/light.png) repeat-y -452px 0}div.cartodb-popup div.cartodb-popup-content{display:block;width:190px;max-width:190px;min-height:5px;height:auto;max-height:185px;margin:0;padding:0;overflow-y:auto;overflow-x:hidden!important;outline:0;text-align:left}div.cartodb-popup .jspContainer:after,div.cartodb-popup .jspContainer:before{content:'';position:absolute;left:0;right:12px;display:block;height:10px;width:190px;z-index:5}div.cartodb-popup .jspContainer:after{bottom:0;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,rgba(255,255,255,0)),color-stop(100%,#fff));background:-webkit-linear-gradient(top,rgba(255,255,255,0),#fff);background:-moz-linear-gradient(top,rgba(255,255,255,0),#fff);background:-o-linear-gradient(top,rgba(255,255,255,0),#fff);background:linear-gradient(top,rgba(255,255,255,0),#fff);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0)}div.cartodb-popup .jspContainer:before{top:0;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#fff),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(top,#fff,rgba(255,255,255,0));background:-moz-linear-gradient(top,#fff,rgba(255,255,255,0));background:-o-linear-gradient(top,#fff,rgba(255,255,255,0));background:linear-gradient(top,#fff,rgba(255,255,255,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0)}div.cartodb-popup div.cartodb-popup-tip-container{width:226px;height:20px;background:url(../img/light.png) no-repeat 0 0}div.cartodb-popup a.cartodb-popup-close-button{position:absolute;top:-9px;right:-9px;width:26px;height:26px;padding:0;background:url(../img/light.png) no-repeat 0 -23px;text-indent:-9999px;font-size:0;line-height:0;opacity:1;-ms-filter:"alpha(Opacity=100)";filter:alpha(Opacity=1);filter:alpha(opacity=100);text-transform:uppercase;z-index:3}div.cartodb-popup.header.no_fields div.cartodb-popup-content{display:none}div.cartodb-popup.header.no_fields div.cartodb-popup-content-wrapper div.cartodb-edit-buttons{padding-top:5px;margin-top:0}div.cartodb-popup.header.no_fields div.cartodb-edit-buttons{border:0;padding-top:0}div.cartodb-popup .jspContainer{overflow:hidden;position:relative;outline:0}div.cartodb-popup .jspContainer *{outline:0}div.cartodb-popup .jspPane{position:absolute;padding:4px 0 0!important;z-index:1}div.cartodb-popup .jspVerticalBar{position:absolute;top:0;right:0;width:6px;height:100%;background:0 0;z-index:10}div.cartodb-popup .jspHorizontalBar{position:absolute;bottom:0;left:0;width:100%;height:6px;background:0 0}div.cartodb-popup .jspHorizontalBar *,div.cartodb-popup .jspVerticalBar *{margin:0;padding:0}div.cartodb-popup .jspCap{display:none}div.cartodb-popup .jspHorizontalBar .jspCap{float:left}div.cartodb-popup .jspTrack{position:relative;cursor:pointer;background:0 0}div.cartodb-popup .jspDrag{position:relative;top:0;left:0;cursor:pointer;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;background:#999;background:rgba(0,0,0,.16)}div.cartodb-popup .jspDrag:hover{background:#666;background:rgba(0,0,0,.5);cursor:pointer}div.cartodb-popup .jspHorizontalBar .jspDrag,div.cartodb-popup .jspHorizontalBar .jspTrack{float:left;height:100%}div.cartodb-popup .jspArrow{background:#50506d;text-indent:-20000px;display:block;cursor:pointer}div.cartodb-popup .jspArrow.jspDisabled{cursor:default;background:#80808d}div.cartodb-popup .jspVerticalBar .jspArrow{height:16px}div.cartodb-popup .jspHorizontalBar .jspArrow{width:16px;float:left;height:100%}div.cartodb-popup .jspVerticalBar .jspArrow:focus{outline:0}div.cartodb-popup .jspCorner{background:#eeeef4;float:left;height:100%}* html div.cartodb-popup .jspCorner{margin:0 -3px 0 0}div.cartodb-popup h1,div.cartodb-popup h2,div.cartodb-popup h3,div.cartodb-popup h4,div.cartodb-popup h5,div.cartodb-popup h6{display:block;width:190px;margin:0;padding:0;font-weight:700;font-family:"Helvetica Neue",Helvetica,Arial;color:#CCC;text-transform:uppercase;word-wrap:break-word;line-height:120%}div.cartodb-popup h1{font-size:24px}div.cartodb-popup h2{font-size:20px}div.cartodb-popup h3{font-size:15px}div.cartodb-popup h4{font-size:11px}div.cartodb-popup h5{font-size:10px}div.cartodb-popup h6{font-size:9px}div.cartodb-popup p{display:block;width:190px;max-width:190px;margin:0;padding:0 0 7px;font:400 13px Helvetica,Arial;word-wrap:break-word}div.cartodb-popup p.italic{font-style:italic}div.cartodb-popup p.loading{position:relative;display:block;width:170px;max-width:170px;margin:0;padding:0 0 0 30px;font:400 13px Helvetica,Arial;font-style:italic;word-wrap:break-word;line-height:21px}div.cartodb-popup p.error{position:relative;display:block;width:170px;max-width:170px;margin:0;padding:0;font:400 13px Helvetica,Arial;font-style:italic;word-wrap:break-word;line-height:18px}div.cartodb-popup p.empty{font-style:italic}div.cartodb-popup div.spinner{position:absolute!important;display:inline;top:0;left:0;margin:10px 0 0 10px}div.cartodb-popup.v2{width:226px;padding:0;margin:0 0 14px;-moz-box-shadow:0 0 0 4px rgba(0,0,0,.15);-webkit-box-shadow:0 0 0 4px rgba(0,0,0,.15);box-shadow:0 0 0 4px rgba(0,0,0,.15);-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;background:#fff}div.cartodb-popup.v2:before{content:'';position:absolute;bottom:-14px;left:0;width:0;height:0;margin-left:28px;border-left:0 solid transparent;border-right:14px solid transparent;border-top:14px solid #fff;z-index:2}div.cartodb-popup.v2 div.cartodb-popup-content-wrapper{width:auto;max-width:none;padding:12px;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;background:0 0}div.cartodb-popup.v2 div.cartodb-popup-content{width:auto;max-width:none;display:block;background:0 0}div.cartodb-popup.v2 div.cartodb-popup-content h1,div.cartodb-popup.v2 div.cartodb-popup-content h2,div.cartodb-popup.v2 div.cartodb-popup-content h3,div.cartodb-popup.v2 div.cartodb-popup-content h4,div.cartodb-popup.v2 div.cartodb-popup-content h5,div.cartodb-popup.v2 div.cartodb-popup-content h6,div.cartodb-popup.v2 div.cartodb-popup-content p{width:auto;max-width:95%;display:block}div.cartodb-popup.v2 div.cartodb-popup-tip-container{position:absolute;bottom:-20px;left:-4px;width:20px;height:16px;margin-left:28px;background:0 0;overflow:hidden;z-index:0}div.cartodb-popup.v2 div.cartodb-popup-tip-container:before{content:'';position:absolute;width:20px;height:20px;left:0;top:-10px;margin-left:0;-ms-transform:skew(0,-45deg);-webkit-transform:skew(0,-45deg);transform:skew(0,-45deg);border-radius:0 0 0 10px;background:rgba(0,0,0,.15);z-index:0}div.cartodb-popup.v2.centered:before{content:'';position:absolute;width:0;height:0;left:-10px;bottom:-10px;margin-left:50%;border-left:10px solid transparent;border-right:10px solid transparent;border-top:10px solid #fff;border-radius:0;-ms-transform:skew(0,0);-webkit-transform:skew(0,0);transform:skew(0,0);background:0 0;z-index:1}div.cartodb-popup.v2.centered p{width:160px;padding-bottom:0}div.cartodb-popup.v2.centered div.cartodb-popup-tip-container{left:-12px;width:24px;margin-left:50%}div.cartodb-popup.v2.centered div.cartodb-popup-tip-container:before{content:'';position:absolute;width:0;height:0;left:0;top:0;margin-left:0;border-left:12px solid transparent;border-right:12px solid transparent;border-top:12px solid rgba(0,0,0,.15);-ms-transform:skew(0,0);-webkit-transform:skew(0,0);transform:skew(0,0);background:0 0;z-index:0}div.cartodb-popup.v2 a.cartodb-popup-close-button{right:-12px;top:-12px;width:20px;height:20px;background:#fff;-webkit-border-radius:18px;-moz-border-radius:18px;border-radius:18px;box-shadow:0 0 0 3px rgba(0,0,0,.15)}div.cartodb-popup.v2 a.cartodb-popup-close-button:after,div.cartodb-popup.v2 a.cartodb-popup-close-button:before{content:'';position:absolute;top:9px;left:6px;width:8px;height:2px;background:#397DBA;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}div.cartodb-popup.v2 a.cartodb-popup-close-button:before{-ms-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}div.cartodb-popup.v2 a.cartodb-popup-close-button:after{-ms-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}div.cartodb-popup.v2 a.cartodb-popup-close-button:hover{box-shadow:0 0 0 3px rgba(0,0,0,.25)}@media \0screen\,screen\9{div.cartodb-popup.v2{border:4px solid #CCC}div.cartodb-popup.v2 div.cartodb-popup-tip-container{position:absolute;width:0;height:0;margin-left:28px;z-index:2;bottom:-18px;left:-4px;border-left:0 solid transparent;border-right:18px solid transparent;border-top:18px solid #fff}div.cartodb-popup.v2 a.cartodb-popup-close-button{right:-14px;top:-14px;width:18px;padding:0 0 0 2px;text-indent:0;font:700 11px Arial;font-weight:700;text-decoration:none;text-align:center;line-height:20px;border:2px solid #CCC}div.cartodb-popup.v2 a.cartodb-popup-close-button:after,div.cartodb-popup.v2 a.cartodb-popup-close-button:before{display:none}div.cartodb-popup.v2 a.cartodb-popup-close-button:hover{border:2px solid #999}}div.cartodb-popup.header.blue div.cartodb-popup-header{background:url(../img/headers.png) no-repeat 0 -40px}div.cartodb-popup.header.blue.header .cartodb-popup-header a{color:#fff}div.cartodb-popup.header.blue div.cartodb-popup-header h4{color:#1F4C7F}div.cartodb-popup.header.blue div.cartodb-popup-header span.separator{background:#225386}div.cartodb-popup.header.blue a.cartodb-popup-close-button{background:url(../img/headers.png) no-repeat -226px -40px}div.cartodb-popup.header.blue a.cartodb-popup-close-button:hover{background-position:-226px -66px}div.cartodb-popup.v2.header.blue div.cartodb-popup-header{background:0 0;background:-ms-linear-gradient(top,#4F9CD7,#2B68A8);background:-o-linear-gradient(right,#4F9CD7,#2B68A8);background:-webkit-linear-gradient(top,#4F9CD7,#2B68A8);background:-moz-linear-gradient(right,#4F9CD7,#2B68A8);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(startColorStr='#4F9CD7',endColorStr='#2B68A8',GradientType=0)"}div.cartodb-popup.v2.header.blue a.cartodb-popup-close-button{background:#fff}div.cartodb-popup.header{padding:0;background:0 0;box-shadow:none;-webkit-box-shadow:none;-moz-box-shadow:none;-o-box-shadow:none;border-bottom:0;border-radius:0;-webkit-border-radius:0;-moz-border-radius:0;-o-border-radius:0}div.cartodb-popup.header div.cartodb-popup-header{position:relative;width:188px;height:auto;max-height:62px;overflow:hidden;padding:17px 19px;background:url(../img/headers.png) no-repeat 0 -40px}div.cartodb-popup.header div.cartodb-popup-header h1{width:100%;margin:0;font:700 21px "Helvetica Neue",Helvetica,Arial;color:#FFF;line-height:23px;text-shadow:0 1px rgba(0,0,0,.5);word-wrap:break-word}div.cartodb-popup.header div.cartodb-popup-header h1 a{color:#fff;font-size:21px;word-wrap:break-word}div.cartodb-popup.header div.cartodb-popup-header h1 a:hover{text-decoration:underline}div.cartodb-popup.header div.cartodb-popup-header h1.loading{position:relative;display:block;width:auto;padding-right:0;padding-left:30px;font-size:14px;font-weight:400;line-height:19px}div.cartodb-popup.header div.cartodb-popup-header h1.error{position:relative;display:block;width:auto;padding-right:0;padding-left:0;font-size:14px;font-weight:400;font-style:italic;line-height:19px}div.cartodb-popup.header div.cartodb-popup-header h4{color:#1F4C7F}div.cartodb-popup.header div.cartodb-popup-header span.separator{position:absolute;bottom:0;left:4px;right:4px;height:1px;background:#225386}div.cartodb-popup.header div.cartodb-popup-content{max-height:150px}div.cartodb-popup.header a.cartodb-popup-close-button{background:url(../img/headers.png) no-repeat -226px -40px}div.cartodb-popup.header a.cartodb-popup-close-button:hover{background-position:-226px -66px}div.cartodb-popup.header.v2.header{-moz-box-shadow:0 0 0 4px rgba(0,0,0,.15);-webkit-box-shadow:0 0 0 4px rgba(0,0,0,.15);box-shadow:0 0 0 4px rgba(0,0,0,.15);-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;background:#fff}div.cartodb-popup.v2.header div.cartodb-popup-header{position:relative;width:auto;height:auto;max-height:62px;overflow:hidden;padding:17px 12px;background:0 0;background:-ms-linear-gradient(top,#4F9CD7,#2B68A8);background:-o-linear-gradient(right,#4F9CD7,#2B68A8);background:-webkit-linear-gradient(top,#4F9CD7,#2B68A8);background:-moz-linear-gradient(right,#4F9CD7,#2B68A8);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(startColorStr='#4F9CD7',endColorStr='#2B68A8',GradientType=0)";-webkit-border-top-left-radius:2px;-webkit-border-top-right-radius:2px;-moz-border-radius-topleft:2px;-moz-border-radius-topright:2px;border-top-left-radius:2px;border-top-right-radius:2px}div.cartodb-popup.v2.header div.cartodb-popup-header:before{content:'';position:absolute;bottom:0;left:0;right:0;width:100%;height:1px;background:rgba(0,0,0,.1)}div.cartodb-popup.v2.header a.cartodb-popup-close-button{right:-12px;top:-12px;width:20px;height:20px;background:#fff;-webkit-border-radius:18px;-moz-border-radius:18px;border-radius:18px;box-shadow:0 0 0 3px rgba(0,0,0,.15)}div.cartodb-popup.v2.header a.cartodb-popup-close-button:after,div.cartodb-popup.v2.header a.cartodb-popup-close-button:before{content:'';position:absolute;top:9px;left:6px;width:8px;height:2px;background:#397DBA;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}div.cartodb-popup.v2.header a.cartodb-popup-close-button:before{-ms-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}div.cartodb-popup.v2.header a.cartodb-popup-close-button:after{-ms-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}div.cartodb-popup.v2.header a.cartodb-popup-close-button:hover{box-shadow:0 0 0 3px rgba(0,0,0,.25)}@media \0screen\,screen\9{div.cartodb-popup.header.v2{border-bottom:4px solid #CCC}div.cartodb-popup.v2.header div.cartodb-popup-header{background:#3B7FBD;-ms-filter:progid:DXImageTransform.Microsoft.Gradient(startColorStr='#4F9CD7', endColorStr='#2B68A8', GradientType=0)}}div.cartodb-popup.header.green div.cartodb-popup-header{background:url(../img/headers.png) no-repeat -252px -40px}div.cartodb-popup.header.green div.cartodb-popup-header h4{color:#00916D}div.cartodb-popup.header.green div.cartodb-popup-header span.separator{background:#008E6A}div.cartodb-popup.header.green a.cartodb-popup-close-button{background:url(../img/headers.png) no-repeat -478px -40px}div.cartodb-popup.header.green a.cartodb-popup-close-button:hover{background-position:-478px -66px}div.cartodb-popup.v2.header.green div.cartodb-popup-header{background:0 0;background:-ms-linear-gradient(top,#0c9,#00B185);background:-o-linear-gradient(right,#0c9,#00B185);background:-webkit-linear-gradient(top,#0c9,#00B185);background:-moz-linear-gradient(right,#0c9,#00B185);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(startColorStr='#00CC99',endColorStr='#00B185',GradientType=0)"}div.cartodb-popup.v2.header.green a.cartodb-popup-close-button{background:#fff}div.cartodb-popup.v2.header.green a.cartodb-popup-close-button:after,div.cartodb-popup.v2.header.green a.cartodb-popup-close-button:before{background:#0C9}@media \0screen\,screen\9{div.cartodb-popup.v2.header.green a.cartodb-popup-close-button{color:#0C9}}div.cartodb-popup.header.orange div.cartodb-popup-header{background:url(../img/headers.png) no-repeat -756px -40px}div.cartodb-popup.header.orange div.cartodb-popup-header h4{color:#CC2929}div.cartodb-popup.header.orange div.cartodb-popup-header span.separator{background:#CC2929}div.cartodb-popup.header.orange a.cartodb-popup-close-button{background:url(../img/headers.png) no-repeat -982px -40px}div.cartodb-popup.header.orange a.cartodb-popup-close-button:hover{background-position:-982px -66px}div.cartodb-popup.v2.header.orange div.cartodb-popup-header{background:0 0;background:-ms-linear-gradient(top,#FF6825,#f33);background:-o-linear-gradient(right,#FF6825,#f33);background:-webkit-linear-gradient(top,#FF6825,#f33);background:-moz-linear-gradient(right,#FF6825,#f33);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(startColorStr='#FF6825',endColorStr='#FF3333',GradientType=0)"}div.cartodb-popup.v2.header.orange a.cartodb-popup-close-button{background:#fff}div.cartodb-popup.v2.header.orange a.cartodb-popup-close-button:after,div.cartodb-popup.v2.header.orange a.cartodb-popup-close-button:before{background:#CC2929}@media \0screen\,screen\9{div.cartodb-popup.v2.header.orange a.cartodb-popup-close-button{color:#CC2929}}div.cartodb-popup.header.with-image div.cartodb-popup-header{position:relative;background:url(../img/headers.png) no-repeat -1008px 0;height:138px;max-height:104px}div.cartodb-popup.header.with-image div.cartodb-popup-header .cover{display:block;position:absolute;overflow:hidden;width:218px;height:135px;top:4px;left:4px;border-radius:4px 4px 0 0}div.cartodb-popup.header.with-image div.cartodb-popup-header .cover .shadow{position:absolute;width:218px;height:55px;bottom:0;left:0;background:url(../img/shadow.png) no-repeat;z-index:100}div.cartodb-popup.header.with-image div.cartodb-popup-header .cover #spinner{position:absolute;top:67px;left:109px}div.cartodb-popup.header.with-image div.cartodb-popup-header .cover img{position:absolute;border-radius:4px 4px 0 0;display:none}div.cartodb-popup.header.with-image div.cartodb-popup-header .image_not_found{position:absolute;top:15px;left:15px;width:200px;display:none}div.cartodb-popup.header.with-image div.cartodb-popup-header .image_not_found a{display:-moz-inline-stack;display:inline-block;vertical-align:top;*vertical-align:auto;zoom:1;*display:inline;margin:3px 0 0 -2px;color:#888;font-size:13px;font-family:Helvetica,"Helvetica Neue",Arial,sans-serif;text-decoration:underline}div.cartodb-popup.header.with-image div.cartodb-popup-header .image_not_found a:hover{color:#888;text-decoration:underline}div.cartodb-popup.header.with-image div.cartodb-popup-header .cover .image_not_found i{display:-moz-inline-stack;display:inline-block;vertical-align:top;*vertical-align:auto;zoom:1;*display:inline;width:31px;height:22px;background:transparent url(../img/image_not_found.png)}div.cartodb-popup.header.with-image div.cartodb-popup-header h1{position:absolute;bottom:13px;left:18px;width:188px;z-index:150}div.cartodb-popup.header.with-image div.cartodb-popup-header h4{color:#CCC}div.cartodb-popup.header.with-image div.cartodb-popup-header span.separator{background:#CCC}div.cartodb-popup.header.with-image a.cartodb-popup-close-button{background:url(../img/headers.png) no-repeat -226px -40px}div.cartodb-popup.header.with-image a.cartodb-popup-close-button:hover{background-position:-226px -66px}div.cartodb-popup.header.with-image .cartodb-popup-header h1{display:none}div.cartodb-popup.header.with-image .cartodb-popup-header h1.order1{display:block}div.cartodb-popup.header.with-image .cartodb-popup-content-wrapper .order1{display:none}div.cartodb-popup.v2.header.with-image div.cartodb-popup-header{background:#2C2C2C;background:-ms-linear-gradient(top,#535353,#2C2C2C);background:-o-linear-gradient(right,#535353,#2C2C2C);background:-webkit-linear-gradient(top,#535353,#2C2C2C);background:-moz-linear-gradient(right,#535353,#2C2C2C);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(startColorStr='#535353',endColorStr='#2C2C2C',GradientType=0)"}div.cartodb-popup.v2.header.with-image div.cartodb-popup-header h1{width:85%}div.cartodb-popup.v2.header.with-image div.cartodb-popup-header span.separator{left:0;right:0;background:#CCC}div.cartodb-popup.v2.header.with-image a.cartodb-popup-close-button{background:#fff}div.cartodb-popup.v2.header.with-image div.cartodb-popup-header .cover{display:block;width:100%;height:138px;top:0;left:0;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;overflow:hidden}div.cartodb-popup.v2.header.with-image div.cartodb-popup-header .cover .shadow{width:100%;height:57px;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,transparent),color-stop(100%,rgba(0,0,0,.8)));background:-webkit-linear-gradient(top,transparent,rgba(0,0,0,.8));background:-moz-linear-gradient(top,transparent,rgba(0,0,0,.8));background:-o-linear-gradient(top,transparent,rgba(0,0,0,.8));background:linear-gradient(top,transparent,rgba(0,0,0,.8));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#000000', GradientType=0)}div.cartodb-popup.v2.header.with-image div.cartodb-popup-header .cover img{-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}div.cartodb-popup.header.yellow div.cartodb-popup-header{background:url(../img/headers.png) no-repeat -504px -40px}div.cartodb-popup.header.yellow div.cartodb-popup-header h4{color:#D8832A}div.cartodb-popup.header.yellow div.cartodb-popup-header span.separator{background:#CC7A29}div.cartodb-popup.header.yellow a.cartodb-popup-close-button{background:url(../img/headers.png) no-repeat -730px -40px}div.cartodb-popup.header.yellow a.cartodb-popup-close-button:hover{background-position:-730px -66px}div.cartodb-popup.v2.header.yellow div.cartodb-popup-header{background:0 0;background:-ms-linear-gradient(top,#FFBF0D,#f93);background:-o-linear-gradient(right,#FFBF0D,#f93);background:-webkit-linear-gradient(top,#FFBF0D,#f93);background:-moz-linear-gradient(right,#FFBF0D,#f93);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(startColorStr='#FFBF0D',endColorStr='#FF9933',GradientType=0)"}div.cartodb-popup.v2.header.yellow a.cartodb-popup-close-button{background:#fff}div.cartodb-popup.v2.header.yellow a.cartodb-popup-close-button:after,div.cartodb-popup.v2.header.yellow a.cartodb-popup-close-button:before{background:#CC7A29}@media \0screen\,screen\9{div.cartodb-popup.v2.header.yellow a.cartodb-popup-close-button{color:#CC7A29}}div.cartodb-popup h4{color:#CCC}div.cartodb-popup p{color:#333}div.cartodb-popup p.loading{color:#888}div.cartodb-popup p.error{color:#FF7F7F}div.cartodb-popup p.empty{color:#999}@-webkit-keyframes loading{to{opacity:1}}@-moz-keyframes loading{to{opacity:1}}@-ms-keyframes loading{to{opacity:1}}@keyframes loading{to{opacity:1}}@-webkit-keyframes pulse{to{opacity:1;-webkit-transform:scale(1)}}@-moz-keyframes pulse{to{opacity:1;-moz-transform:scale(1)}}@-ms-keyframes pulse{to{opacity:1;-ms-transform:scale(1)}}@keyframes pulse{to{opacity:1;transform:scale(1)}}div.cartodb-share{display:none;position:relative;float:right;margin:20px 20px 0 0;z-index:105}div.cartodb-share a{width:14px;height:14px;display:block;color:#397DB8;font-size:10px;font-weight:700;text-transform:uppercase;text-shadow:none;padding:7px;box-sizing:content-box;background:#fff url(../img/share.png) no-repeat 7px 8px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999}div.cartodb-share a:hover{background:#fff url(../img/share.png) no-repeat -28px 8px}div.cartodb-share a:active,div.cartodb-share a:hover:active{background:#fff url(../img/share.png) no-repeat 7px 8px}.cartodb-fullscreen{display:none;position:relative;margin:11px 0 0 20px;float:left;clear:both;z-index:105}.cartodb-fullscreen a{display:block;width:14px;height:14px;padding:7px;box-sizing:content-box;background:#fff url(../img/fullscreen.png) no-repeat 7px 3px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999}.cartodb-fullscreen a:active{background-position:7px 3px!important}.cartodb-fullscreen a:hover{background-position:-19px 5px}.cartodb-share-dialog{display:none}.cartodb-share-dialog .mamufas{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);cursor:default;z-index:1000001}.cartodb-share-dialog .modal{position:absolute;top:50%;left:50%;margin-left:-216px;margin-top:-107px;webkit-box-shadow:rgba(0,0,0,.15) 0 0 0 4px;-moz-box-shadow:rgba(0,0,0,.15) 0 0 0 4px;box-shadow:rgba(0,0,0,.15) 0 0 0 4px;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;font-weight:700;font-family:"Segoe UI Bold","Helvetica Bold",Helvetica,Arial;color:#333;line-height:normal}.cartodb-share-dialog.small .modal{margin-left:-108px;margin-top:-165px}.cartodb-share-dialog.small .block .buttons{margin:0 0 10px}.cartodb-share-dialog.small .block .buttons ul{border:0;padding:0}.cartodb-share-dialog.small .block .content .embed_code{padding:0}.cartodb-share-dialog .modal a.close{position:absolute;top:-15px;right:-15px;width:30px;height:15px;padding:7px 0 8px;background:#fff;font:400 13px Helvetica,Arial;text-decoration:none;webkit-box-shadow:rgba(0,0,0,.15) 0 0 0 4px;-moz-box-shadow:rgba(0,0,0,.15) 0 0 0 4px;box-shadow:rgba(0,0,0,.15) 0 0 0 4px;-webkit-border-radius:50px;-moz-border-radius:50px;-ms-border-radius:50px;-o-border-radius:50px;border-radius:50px;line-height:14px;text-align:center;z-index:105}.cartodb-share-dialog .block{background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;webkit-box-shadow:rgba(0,0,0,.15) 0 0 4px 3px;-moz-box-shadow:rgba(0,0,0,.15) 0 0 4px 3px;box-shadow:rgba(0,0,0,.15) 0 0 4px 3px}.cartodb-share-dialog .block .buttons ul{margin:0;padding:0 24px 0 0;border-right:1px solid #E5E5E5}.cartodb-share-dialog .block .buttons li{list-style:none;margin:0 0 4px;padding:0}.cartodb-share-dialog .block .buttons li a{display:block;padding:10px 13px 11px 30px;width:121px;font-size:13px;font-weight:700;color:#fff;background:#3D8FCA;text-decoration:none;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px}@media only screen and (min-device-width:320px) and (max-device-width:480px){div.cartodb-header h1{width:78%}div.cartodb-header>p{width:80%}}@media only screen and (min-device-width:768px) and (max-device-width:1024px){div.cartodb-header h1{width:78%}div.cartodb-header>p{width:80%}}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min--moz-device-pixel-ratio:2),only screen and (-o-min-device-pixel-ratio:2 / 1),only screen and (min-device-pixel-ratio:2),only screen and (min-resolution:192dpi),only screen and (min-resolution:2dppx){div.cartodb-header h1{width:78%}div.cartodb-header>p{width:80%}div.cartodb-zoom a{background:url(../img/other@2x.png) no-repeat 0 0!important;background-size:113px 34px!important}div.cartodb-zoom a.zoom_in{background-position:-68px 9px!important}div.cartodb-zoom a.zoom_out{background-position:-94px 10px!important}div.cartodb-header div.social a.facebook{background:url(../img/other@2x.png) no-repeat 0 0!important;background-size:113px 34px!important}div.cartodb-header div.social a.twitter{background:url(../img/other@2x.png) no-repeat -26px 0!important;background-size:113px 34px!important}div.cartodb-searchbox span.loader{background:url(../img/loader@2x.gif) no-repeat center center #fff!important;background-size:16px 16px!important}div.cartodb-mobile .aside div.cartodb-searchbox span.loader{background:url(../img/dark_loader@2x.gif) no-repeat center center #292929!important;background-size:16px 16px!important}div.cartodb-tiles-loader div.loader{background:url(../img/loader@2x.gif) no-repeat center center #fff!important;background-size:16px 16px!important}div.cartodb-searchbox input.submit{background:url(../img/other@2x.png) no-repeat -56px 0!important;background-size:113px 34px!important}.cartodb-mobile .aside .cartodb-searchbox input.submit{background:url(../img/mobile_zoom.png) no-repeat 0 0!important}.cartodb-mobile div.cartodb-slides-controller div.slides-controller-content a.prev{background:url(../img/slide_left@2x.png) no-repeat;background-size:16px 15px}.cartodb-mobile div.cartodb-slides-controller div.slides-controller-content a.next{background:url(../img/slide_right@2x.png) no-repeat;background-size:16px 15px}}.cartodb-share-dialog .block .buttons li a.twitter{background:#3D8FCA url(../img/twitter.png) no-repeat 10px 50%}.cartodb-share-dialog .block .buttons li a.twitter:hover{background-color:#3272A0}.cartodb-share-dialog .block .buttons li a.facebook{background:#3B5998 url(../img/facebook.png) no-repeat 10px 50%}.cartodb-share-dialog .block .buttons li a.facebook:hover{background-color:#283C65}.cartodb-share-dialog .block .buttons li a.link{background:#f37f7b url(../img/link.png) no-repeat 10px 50%}.cartodb-share-dialog .block .buttons li a.link:hover{background-color:#DC6161}.cartodb-share-dialog .block a,.cartodb-share-dialog .block h3,.cartodb-share-dialog .block label,.cartodb-share-dialog .block p{letter-spacing:0}.cartodb-share-dialog .block div.head{position:relative;padding:5px 26px;border-bottom:1px solid #E5E5E5}.cartodb-share-dialog .block h3{margin:1em 0;font-size:15px;font-weight:700}.cartodb-share-dialog .block h4{font-size:13px;font-weight:700;color:#666;padding:0;margin:0;margin:0 0 9px}.cartodb-share-dialog .block .content .buttons,.cartodb-share-dialog .block .content .embed_code{display:inline-block;zoom:1;*display:inline;vertical-align:top}.cartodb-share-dialog .block .content .embed_code{padding-left:24px}.cartodb-share-dialog .block .content .embed_code textarea{resize:none;padding:5px;width:153px;height:104px;border:1px solid #C3C3C3;background:#F5F5F5;font-size:11px;color:#666;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.cartodb-share-dialog .block .content{padding:20px 26px 30px}.cartodb-mobile{width:100%;height:100%;z-index:100000000}.cartodb-mobile .cartodb-header{background:0 0;z-index:100000}.cartodb-mobile .cartodb-header .content{padding:0}.cartodb-mobile .cartodb-header .hgroup{position:relative;height:40px;padding:10px}.cartodb-mobile.with-fullscreen .cartodb-header .hgroup{position:relative;margin-left:60px;margin-right:70px}.cartodb-mobile.with-header .cartodb-header .content .hgroup .description,.cartodb-mobile.with-header .cartodb-header .content .hgroup .title{display:block}.cartodb-mobile .cartodb-header .content .description,.cartodb-mobile .cartodb-header .content .title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cartodb-mobile .cartodb-header .content .button{height:58px;width:58px;background-color:rgba(0,0,0,.5);line-height:normal;z-index:99999}.cartodb-mobile.with-header .cartodb-header,.cartodb-mobile.with-slides .cartodb-header{background-color:rgba(0,0,0,.5)}.cartodb-mobile.with-fullscreen .cartodb-header .content .fullscreen{display:block}.cartodb-mobile.with-header .cartodb-header .content .fullscreen{background:0 0}.cartodb-mobile .cartodb-header .content .fullscreen{display:none;position:relative;top:0;left:0;float:left;width:60px;height:60px;margin:auto;padding:0;background:rgba(0,0,0,.5);cursor:pointer;z-index:10;-webkit-border-radius:0 0 5px;-moz-border-radius:0 0 5px;-ms-border-radius:0 0 5px;-o-border-radius:0 0 5px;border-radius:0 0 5px;-webkit-transform-style:"ease-in";-moz-transform-style:"ease-in";-ms-transform-style:"ease-in";-o-transform-style:"ease-in";transform-style:"ease-in";-webkit-transition-property:background;-moz-transition-property:background;-o-transition-property:background;transition-property:background;-webkit-transition-duration:150ms;-moz-transition-duration:150ms;-o-transition-duration:150ms;transition-duration:150ms}.cartodb-mobile.with-header .cartodb-header .content .fullscreen{border-right:1px solid rgba(255,255,255,.35);-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0}.cartodb-mobile .cartodb-header .content .fullscreen:hover,.cartodb-mobile.with-header .cartodb-header .content .fullscreen:hover{background:rgba(0,0,0,.3)}.cartodb-mobile .cartodb-header .content .fullscreen:before{content:'';width:60px;height:60px;background:url(../img/fullscreen_mobile.png) no-repeat 50% 50%;background-size:28px 28px;position:absolute}.cartodb-mobile.with-layers .cartodb-header .content .toggle,.cartodb-mobile.with-search .cartodb-header .content .toggle{display:block}.cartodb-mobile .cartodb-header .content .toggle{display:none;position:relative;top:0;right:0;float:right;width:70px;height:60px;margin:auto;padding:0;background:rgba(0,0,0,.5);cursor:pointer;z-index:10;-webkit-border-radius:0 0 0 5px;-moz-border-radius:0 0 0 5px;-ms-border-radius:0 0 0 5px;-o-border-radius:0 0 0 5px;border-radius:0 0 0 5px;-webkit-transform-style:"ease-in";-moz-transform-style:"ease-in";-ms-transform-style:"ease-in";-o-transform-style:"ease-in";transform-style:"ease-in";-webkit-transition-property:background;-moz-transition-property:background;-o-transition-property:background;transition-property:background;-webkit-transition-duration:150ms;-moz-transition-duration:150ms;-o-transition-duration:150ms;transition-duration:150ms}.cartodb-mobile .cartodb-header .content .toggle:hover,.cartodb-mobile.with-header .cartodb-header .content .toggle:hover{background:rgba(0,0,0,.3)}.cartodb-mobile.with-header .cartodb-header .content .toggle{background:0 0;border-left:1px solid rgba(255,255,255,.35);-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0}.cartodb-mobile .cartodb-header .content .toggle:before{content:'';width:70px;height:60px;background:url(../img/toggle_aside.png) no-repeat 50% 50%;background-size:30px 30px;position:absolute}.cartodb-mobile.with-zoom .cartodb-zoom{float:left;position:relative;z-index:100000}.cartodb-mobile .aside{position:absolute;width:250px;height:100%;top:0;right:-250px;background:#2D2D2D;cursor:default;z-index:1000010}.cartodb-mobile .aside .cartodb-searchbox{position:relative;display:none;float:none;margin:0;width:100%;height:auto;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background:0 0;-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0;border:0;border-bottom:1px solid #505050;z-index:105}.cartodb-mobile .aside .cartodb-searchbox input.text{border:0;position:initial;top:initial;left:initial;height:39px;padding:10px 18px;width:185px;font-size:13px;color:#fff}.cartodb-mobile .aside .cartodb-searchbox input.text::-webkit-input-placeholder{font-style:italic}.cartodb-mobile .aside .cartodb-searchbox input.text:-moz-placeholder{font-style:italic}.cartodb-mobile .aside .cartodb-searchbox input.text::-moz-placeholder{font-style:italic}.cartodb-mobile .aside .cartodb-searchbox input.text:-ms-input-placeholder{font-style:italic}.cartodb-mobile .aside .cartodb-searchbox span.loader{left:initial;top:18px;right:14px;background:url(../img/dark_loader.gif) no-repeat center center}.cartodb-mobile .aside .cartodb-searchbox input.submit{right:18px;top:23px;width:14px;height:14px;left:initial;outline:0;cursor:pointer;background:url(../img/mobile_zoom.png) no-repeat 0 0}.cartodb-mobile .aside .layer-container{position:relative;height:100%}.cartodb-mobile .aside .scrollpane{width:100%;height:100%;overflow:hidden;outline:0}.cartodb-mobile .aside .scrollpane .jspContainer{overflow:hidden;position:relative}.cartodb-mobile .aside .scrollpane .jspPane{position:absolute}.cartodb-mobile .aside .scrollpane .jspVerticalBar{position:absolute;top:0;right:7px;width:5px;height:100%;background:0 0;z-index:20}.cartodb-mobile .aside .scrollpane .jspVerticalBar *{margin:0;padding:0}.cartodb-mobile .aside .scrollpane .jspCap{display:none}.cartodb-mobile .aside .scrollpane .jspTrack{background:0 0;position:relative}.cartodb-mobile .aside .scrollpane .jspDrag{background:rgba(187,187,187,.5);border-radius:5px;position:relative;top:0;left:0;cursor:pointer}.cartodb-mobile .aside .scrollpane .jspArrow{background:0 0;text-indent:-20000px;display:block;cursor:pointer}.cartodb-mobile .aside .scrollpane .jspVerticalBar .jspArrow{height:10px}.cartodb-mobile .aside .scrollpane .jspVerticalBar .jspArrow:focus{outline:0}.cartodb-mobile .aside .scrollpane .jspCorner{background:#eeeef4;float:left;height:100%}.cartodb-mobile .aside .layer-container>h3{padding:23px 20px;color:#999;font:700 12px Helvetica,Arial,sans-serif;text-transform:uppercase;background:#292929;border-bottom:1px solid #585858}.cartodb-mobile .aside .layer-container .layers{margin:0;padding:0 10px}.cartodb-mobile .aside .layer-container .layers>li{padding:5px 10px;color:#fff;list-style:none;border-bottom:1px solid #585858}.cartodb-mobile .aside .layer-container .layers>li:last-child,.cartodb-mobile .aside .layer-container .layers>li:last-child h3{border:0}.cartodb-mobile .aside .layer-container .layers>li a.toggle{width:21px;height:10px;background:#191919;position:relative;top:2px;float:right;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.cartodb-mobile .aside .layer-container .layers>li a.toggle.hide{display:none}.cartodb-mobile .aside .layer-container .layers>li.hidden a.toggle:before{left:0}.cartodb-mobile .aside .layer-container .layers>li a.toggle:before{position:absolute;content:'';top:1px;right:0;width:7px;height:7px;-webkit-border-radius:100px;-moz-border-radius:100px;-ms-border-radius:100px;-o-border-radius:100px;border-radius:100px;background:#fff}.cartodb-mobile .aside .layer-container .layers>li h3{font:700 12px Helvetica,Arial,sans-serif;text-transform:uppercase;padding:12px 0 13px}.cartodb-mobile .aside .layer-container .layers>li.has-toggle h3{cursor:pointer}.cartodb-mobile .aside .layer-container .layers>li.has-legend.hidden h3,.cartodb-mobile .aside .layer-container .layers>li.hidden h3{color:#666;border:0;padding:12px 0 13px}.cartodb-mobile .aside .layer-container .layers>li.hidden.has-legend div.cartodb-legend{display:none!important}.cartodb-mobile .aside .layer-container .layers>li.hidden.has-legend h3{margin-bottom:0}.cartodb-mobile .aside .layer-container .layers>li.has-legend h3{border-bottom:1px solid #585858}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend{position:relative;border:0;webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background:0 0;margin:10px 0 18px;padding:2px 0 0;bottom:auto;right:auto;cursor:text}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend.bubble ul li.graph{border:0}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend.bubble ul li.graph .bubbles{background:url(../img/dark_bubbles.png) no-repeat 0 0}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend .graph{border:1px solid #1A1108}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend ul li{height:auto;padding:0;font-size:12px;color:#fff;font-weight:400;font-family:Helvetica,Arial,sans-serif;text-transform:none;line-height:normal}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend.intensity ul li.graph{height:22px}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend ul li .bullet{margin-top:2px}.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend ul li.max,.cartodb-mobile .aside .layer-container .layers>li div.cartodb-legend ul li.min{font-size:10px}.cartodb-mobile div.cartodb-timeslider .slider-wrapper{position:absolute;top:17px}.cartodb-mobile div.cartodb-timeslider .slider{width:100%}.cartodb-mobile div.cartodb-timeslider{height:40px;margin-bottom:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0;border:1px solid #E5E5E5;border-left:0;border-right:0;border-top:1px solid rgba(0,0,0,.2);z-index:1000001}.cartodb-mobile div.cartodb-timeslider .slider-wrapper{display:block;width:100%;height:4px;padding:0}.cartodb-mobile div.cartodb-timeslider{width:100%!important}.cartodb-mobile div.cartodb-timeslider ul{width:100%;position:relative;clear:both;overflow:hidden}.cartodb-mobile div.cartodb-timeslider ul li{display:block;background:#fff;float:left}.cartodb-mobile div.cartodb-timeslider ul li.controls{width:50px}.cartodb-mobile div.cartodb-timeslider ul li.time{width:120px}.cartodb-mobile div.cartodb-timeslider ul li.last{position:absolute;left:180px;right:10px}.cartodb-mobile div.cartodb-timeslider ul li.controls a.button{-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0}.cartodb-mobile .cartodb-attribution{display:none;list-style:none;background:#fff;position:absolute;padding:9px 12px;margin:0;right:20px;bottom:20px;color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;z-index:10000001;font:12px/1.5 "Helvetica Neue",Arial,Helvetica,sans-serif}.cartodb-mobile .cartodb-attribution a{color:#0078A8}.cartodb-mobile .cartodb-attribution li{padding:0;margin:3px;display:inline-block;zoom:1;*display:inline;vertical-align:top;color:#999}.cartodb-mobile .cartodb-attribution li a{text-transform:capitalize;color:#0078A8}.cartodb-mobile .backdrop{display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:#000;filter:alpha(opacity=20);filter:alpha(Opacity=20);opacity:.2;z-index:10000000}.cartodb-mobile.with-torque .cartodb-attribution-button{bottom:59px}.cartodb-mobile .cartodb-attribution-button{display:none;width:20px;height:20px;position:absolute;right:20px;bottom:20px;color:#999;text-align:center;text-decoration:none;-webkit-border-radius:100px;-moz-border-radius:100px;-ms-border-radius:100px;-o-border-radius:100px;border-radius:100px;background:#fff url(../img/bg-attribution-button.png) no-repeat 49% 50%;font:12px/1.5 "Helvetica Neue",Arial,Helvetica,sans-serif;z-index:10}.cartodb-mobile .cartodb-attribution-button:before{position:absolute;content:'';top:-3px;left:-3px;width:20px;height:20px;border:3px solid rgba(0,0,0,.3);-webkit-border-radius:100px;-moz-border-radius:100px;-ms-border-radius:100px;-o-border-radius:100px;border-radius:100px;-webkit-transform-style:"ease-in";-moz-transform-style:"ease-in";-ms-transform-style:"ease-in";-o-transform-style:"ease-in";transform-style:"ease-in";-webkit-transition-property:border;-moz-transition-property:border;-o-transition-property:border;transition-property:border;-webkit-transition-duration:150ms;-moz-transition-duration:150ms;-o-transition-duration:150ms;transition-duration:150ms}.cartodb-mobile .cartodb-attribution-button:hover:before{border:3px solid rgba(0,0,0,.7)}.cartodb-mobile .cartodb-slides-controller{position:absolute;bottom:0;top:auto;padding:0;line-height:0;z-index:9}.cartodb-mobile .cartodb-slides-controller .slides-controller-content{padding:20px 0}.cartodb-mobile .cartodb-slides-controller .slides-controller-content .prev{margin:0 20px 0 0}.cartodb-mobile .cartodb-slides-controller .slides-controller-content .next{margin:0 0 0 20px}.cartodb-mobile .cartodb-slides-controller .slides-controller-content .next:before,.cartodb-mobile .cartodb-slides-controller .slides-controller-content .prev:after,.cartodb-mobile .cartodb-slides-controller .slides-controller-content ul{display:none}div.cartodb-legend-stack{position:absolute;bottom:35px;right:20px;display:none;webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;background:#fff;z-index:105;cursor:text}div.cartodb-legend-stack div.cartodb-legend{position:relative;top:auto;right:auto;left:auto;bottom:auto;background:0 0;border:0;margin:0;-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0;border-bottom:1px solid #999;webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;cursor:text}div.cartodb-legend-stack div.cartodb-legend:last-child{border-bottom:0}div.cartodb-legend{position:absolute;bottom:35px;right:20px;padding:13px 15px 14px;font:400 13px Helvetica,Arial;color:#858585;text-align:left;webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;background:#fff;z-index:105}div.cartodb-legend .legend-title{margin:0 0 10px;text-align:left;color:#666;font-weight:700;font-size:11px;text-transform:uppercase}div.cartodb-legend ul{padding:0;margin:0;list-style:none}div.cartodb-legend ul li{padding:0;margin:0;font-size:10px;color:#666;font-weight:700;font-family:Helvetica,Arial;text-transform:uppercase;line-height:normal}div.cartodb-legend-stack div.cartodb-legend.none,div.cartodb-legend.none{display:none}div.map div.cartodb-legend-stack div.cartodb-legend.wrapper .cartodb-legend{padding:0;display:block}div.cartodb-legend.wrapper .cartodb-legend{display:block;padding:0}div.cartodb-legend.category ul li,div.cartodb-legend.color ul li,div.cartodb-legend.custom ul li{position:relative;margin:0 0 7px;font-size:10px;color:#666;font-weight:700;font-family:Helvetica,Arial;text-transform:uppercase;text-align:left;height:10px;line-height:10px;vertical-align:middle}div.cartodb-legend.category ul li.bkg,div.cartodb-legend.color ul li.bkg,div.cartodb-legend.custom ul li.bkg{height:20px;line-height:24px;margin:0 0 15px}div.cartodb-legend.category ul li.bkg .bullet,div.cartodb-legend.color ul li.bkg .bullet,div.cartodb-legend.custom ul li.bkg .bullet{height:20px;width:20px;border:1px solid rgba(0,0,0,.3);border:0;background-size:26px 26px!important;background-position:center center!important;-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0}div.cartodb-legend.category ul li.bkg:last-child,div.cartodb-legend.color ul li.bkg:last-child,div.cartodb-legend.custom ul li.bkg:last-child{margin:0 0 5px}div.cartodb-legend.category ul li:last-child,div.cartodb-legend.color ul li:last-child,div.cartodb-legend.custom ul li:last-child{margin:0}div.cartodb-legend.category ul li .bullet,div.cartodb-legend.color ul li .bullet,div.cartodb-legend.custom ul li .bullet{float:left;margin:0 5px 0 0;width:3px;height:3px;-webkit-border-radius:50%;-moz-border-radius:50%;-ms-border-radius:50%;-o-border-radius:50%;border-radius:50%;padding:2px;background:#fff;border:1px solid rgba(0,0,0,.2);z-index:1000}div.cartodb-legend.bubble{text-align:center}div.cartodb-legend.bubble ul{clear:both;overflow:hidden;display:-moz-inline-stack;display:inline-block}div.cartodb-legend.bubble ul li{position:relative;float:left;top:15px}div.cartodb-legend.bubble ul li.graph{top:0;width:120px;height:40px;margin:0 10px;background:#f1f1f1}div.cartodb-legend.bubble ul li.graph .bubbles{background:url(../img/bubbles.png) no-repeat 0 0;width:120px;height:40px}div.cartodb-legend.choropleth{padding:13px 15px 15px}div.cartodb-legend.choropleth ul{min-width:210px}div.cartodb-legend.choropleth li.min{float:left;margin:0 0 5px}div.cartodb-legend.choropleth li.max{float:right;margin:0 0 5px}div.cartodb-legend.choropleth li.graph div{width:10px;height:22px}div.cartodb-legend.choropleth li.graph .quartile{display:table-cell}div.cartodb-legend.choropleth li.graph.count_7 .quartile{width:30px}div.cartodb-legend.choropleth li.graph.count_5 .quartile{width:42px}div.cartodb-legend.choropleth li.graph.count_3 .quartile{width:70px}div.cartodb-legend.choropleth li.graph .colors{display:table-row}div.cartodb-legend.choropleth li.graph{clear:both;overflow:hidden;display:table;width:100%;height:22px;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding;border:1px solid #b3b3b3}div.cartodb-legend.density{padding:13px 15px 15px}div.cartodb-legend.density ul{min-width:210px}div.cartodb-legend.density li.min{float:left;margin:0 0 5px}div.cartodb-legend.density li.max{float:right;margin:0 0 5px}div.cartodb-legend.density li.graph div{width:10px;height:22px}div.cartodb-legend.density li.graph .quartile{display:table-cell}div.cartodb-legend.density li.graph.count_7 .quartile{width:30px}div.cartodb-legend.density li.graph.count_5 .quartile{width:42px}div.cartodb-legend.density li.graph.count_3 .quartile{width:70px}div.cartodb-legend.density li.graph .colors{display:table-row}div.cartodb-legend.density li.graph{clear:both;overflow:hidden;display:table;width:100%;height:22px;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding;border:1px solid #b3b3b3}div.cartodb-legend.intensity{padding:13px 15px 15px}div.cartodb-legend.intensity ul{min-width:210px}div.cartodb-legend.intensity li.min{float:left;margin:0 0 5px}div.cartodb-legend.intensity li.max{float:right;margin:0 0 5px}div.cartodb-legend.intensity li.graph{clear:both;width:100%;height:22px;background:#f1f1f1;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding;-webkit-box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);-o-box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);-moz-box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);-ms-box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)}div.cartodb-zoom{position:relative;float:left;display:block;margin:20px 0 0 20px;width:28px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;z-index:105}div.cartodb-zoom a{position:relative;display:block;width:28px;height:28px;padding:0;font:700 20px Arial;color:#999;text-align:center;text-decoration:none;text-indent:-9999px;line-height:0;font-size:0;background:url(../img/other.png) no-repeat 0 0}div.cartodb-zoom a.zoom_in{border-bottom:1px solid #E6E6E6;background-position:-68px 10px;-webkit-border-top-left-radius:4px;-webkit-border-top-right-radius:4px;-moz-border-radius-topleft:4px;-moz-border-radius-topright:4px;border-top-left-radius:4px;border-top-right-radius:4px}div.cartodb-zoom a.zoom_in:hover{background-position:-68px -14px;cursor:pointer}div.cartodb-zoom a.zoom_out{background-position:-94px 10px;-webkit-border-bottom-left-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-bottomright:4px;border-bottom-left-radius:4px;border-bottom-right-radius:4px}div.cartodb-zoom a.zoom_out:hover{background-position:-94px -14px;cursor:pointer}div.cartodb-zoom a.disabled{filter:alpha(opacity=20);filter:alpha(Opacity=20);opacity:.2}div.cartodb-zoom a.disabled:hover{cursor:default;color:#999}div.cartodb-zoom-info{position:absolute;display:block;top:100px;left:20px;margin:20px 0 0;width:28px;height:28px;font:400 13px Helvetica,Arial;color:#858585;text-align:center;line-height:28px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;background:#fff;z-index:105}div.cartodb-tiles-loader{float:left;display:block;clear:both}div.cartodb-tiles-loader div.loader{position:relative;display:block;margin:15px 0 0 20px;width:28px;height:28px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;background:url(../img/loader.gif) no-repeat center center #fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;z-index:105}div.cartodb-layer-selector-box{display:none;position:relative;float:right;margin:20px 20px 0 0;width:142px;height:29px;color:#CCC;font-size:13px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;z-index:100000}div.cartodb-layer-selector-box a.layers{float:left;width:126px;padding:6px 8px;line-height:20px;color:#CCC;text-decoration:none;font-family:robotoregular,Helvetica,Arial,Sans-serif}div.cartodb-layer-selector-box a.layers:hover{color:#bbb}div.cartodb-layer-selector-box a.layers:hover .count{background:#ccc}div.cartodb-layer-selector-box a.layers .count{position:absolute;right:6px;top:6px;width:auto;padding:3px 6px;margin:0;font-size:10px;color:#fff;line-height:12px;background:#DDD;-webkit-border-radius:2px;-moz-border-radius:2px;-ms-border-radius:2px;-o-border-radius:2px;border-radius:2px}div.cartodb-layer-selector-box div.cartodb-dropdown{padding:0;margin:0}div.cartodb-layer-selector-box div.cartodb-dropdown ul{padding:0;margin:0;list-style:none;border:1px solid 999999}div.cartodb-layer-selector-box div.cartodb-dropdown ul li{border-bottom:1px solid #EDEDED;position:relative}div.cartodb-layer-selector-box div.cartodb-dropdown ul li:last-child{border-bottom:0}div.cartodb-layer-selector-box div.cartodb-dropdown ul li:hover{background:#fff}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.layer{display:-moz-inline-stack;display:inline-block;vertical-align:middle;width:104px;padding:13px 13px 15px;zoom:1;color:#666;font:400 13px "Helvetica Neue",Helvetica,Arial;text-decoration:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}div.cartodb-layer-selector-box div.cartodb-dropdown ul li:hover a.layer{text-decoration:underline;color:#545454}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch{position:absolute;top:13px;right:10px;text-indent:-9999px;vertical-align:middle;width:23px;height:12px;padding:0;-webkit-border-radius:12px;-moz-border-radius:12px;-ms-border-radius:12px;-o-border-radius:12px;border-radius:12px;-webkit-transform-style:"linear";-moz-transform-style:"linear";-ms-transform-style:"linear";-o-transform-style:"linear";transform-style:"linear";-webkit-transition-property:left;-moz-transition-property:left;-o-transition-property:left;transition-property:left;-webkit-transition-duration:180ms;-moz-transition-duration:180ms;-o-transition-duration:180ms;transition-duration:180ms;text-decoration:none;border:1px solid #44759E}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch:before{position:absolute;content:' ';top:0;left:0;width:100%;height:100%;-webkit-border-radius:12px;-moz-border-radius:12px;-ms-border-radius:12px;-o-border-radius:12px;border-radius:12px;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,rgba(0,0,0,.18)),color-stop(100%,transparent));background:-webkit-linear-gradient(rgba(0,0,0,.18),transparent);background:-moz-linear-gradient(rgba(0,0,0,.18),transparent);background:-o-linear-gradient(rgba(0,0,0,.18),transparent);background:linear-gradient(rgba(0,0,0,.18),transparent);z-index:0}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch span.handle{position:absolute;top:0;width:10px;height:10px;-webkit-border-radius:12px;-moz-border-radius:12px;-ms-border-radius:12px;-o-border-radius:12px;border-radius:12px;border:1px solid #44759e;background:#F2F2F2;z-index:2;-webkit-transform-style:"linear";-moz-transform-style:"linear";-ms-transform-style:"linear";-o-transform-style:"linear";transform-style:"linear";-webkit-transition-property:left;-moz-transition-property:left;-o-transition-property:left;transition-property:left;-webkit-transition-duration:180ms;-moz-transition-duration:180ms;-o-transition-duration:180ms;transition-duration:180ms}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch.enabled{border-color:#44759E;background:#56AFEF}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch.enabled span.handle{left:12px;border-color:#44759E}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch.disabled{opacity:1;-ms-filter:"alpha(Opacity=100)";filter:alpha(Opacity=1);filter:alpha(opacity=100);border-color:#CCC;background:#D8D8D8}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch span.handle{left:0;border-color:#999}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch:hover{cursor:pointer!important}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch.working{opacity:.5;-ms-filter:"alpha(Opacity=50)";filter:alpha(Opacity=.5);filter:alpha(opacity=50)}div.cartodb-layer-selector-box div.cartodb-dropdown ul li a.switch.working:hover{cursor:default!important}div.cartodb-searchbox{position:relative;display:none;float:right;margin:20px 20px 0 0;width:142px;height:29px;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;z-index:105}div.cartodb-searchbox span.loader{position:absolute;display:none;top:3px;left:3px;width:22px;height:22px;background:url(../img/loader.gif) no-repeat center center #fff;z-index:105}div.cartodb-searchbox input.text{position:absolute;top:6px;left:30px;width:103px;padding:0;margin:0;line-height:17px;border:0;background:0 0;border-bottom:1px dotted #CCC;-webkit-border-radius:0;-moz-border-radius:0;-ms-border-radius:0;-o-border-radius:0;border-radius:0;font:400 14px Arial;color:#999;text-align:left;z-index:2}div.cartodb-searchbox input.text:focus{outline:0;border-color:#999;color:#666}div.cartodb-searchbox input.submit{position:absolute;left:8px;top:8px;width:12px;height:12px;text-indent:-9999px;font-size:0;line-height:0;text-transform:uppercase;border:0;background:url(../img/other.png) no-repeat -56px 0;z-index:1}div.cartodb-searchbox input.submit:hover{cursor:pointer}div.cartodb-infobox{padding:20px;position:absolute;display:inline-block;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;text-align:left;z-index:105}div.cartodb-dropdown{position:absolute;display:none;background:#fff;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;border:0;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 1px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 1px;-ms-box-shadow:rgba(0,0,0,.2) 0 0 4px 1px;-o-box-shadow:rgba(0,0,0,.2) 0 0 4px 1px;box-shadow:rgba(0,0,0,.2) 0 0 4px 1px;z-index:150}div.cartodb-dropdown.border{border:1px solid #999}div.cartodb-dropdown div.tail{position:absolute;top:-6px;right:10px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #999;z-index:0}div.cartodb-dropdown div.tail span.border{position:absolute;top:1px;left:-6px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;z-index:2}div#cartodb-gmaps-attribution{position:absolute;display:block;bottom:13px;right:0;height:10px;line-height:10px;padding:0 6px 4px;background:#fff;background:rgba(245,245,245,.7);font-family:Roboto,Arial,sans-serif!important;font-size:11px;font-weight:400;color:#444!important;white-space:nowrap;direction:ltr;text-align:right;background-position:initial initial;background-repeat:initial initial;border:0;z-index:10000}div#cartodb-gmaps-attribution a{color:#444;text-decoration:none}div.cartodb-timeslider{position:absolute;display:inline-block;height:40px;width:auto!important;margin-bottom:30px;padding:0;-webkit-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;-moz-box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;box-shadow:rgba(0,0,0,.2) 0 0 4px 2px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #999;text-align:left;z-index:105}div.cartodb-timeslider ul{display:block;height:40px;margin:0;padding:0;line-height:40px;list-style:none;cursor:default}div.cartodb-timeslider ul li{display:inline-block;zoom:1;*display:inline;vertical-align:top;height:40px;_height:40px;width:auto;line-height:40px;border-right:1px solid #E5E5E5}div.cartodb-timeslider ul li.last{border-right:0}div.cartodb-timeslider a.button{display:block;width:48px;height:40px;text-indent:-9999px;line-height:0;font-size:0;background:url(../img/slider.png) no-repeat -2px -55px}div.cartodb-timeslider a.button:hover{background-position:-42px -55px}div.cartodb-timeslider a.button.stop{background-position:-2px -4px}div.cartodb-timeslider a.button.stop:hover{background-position:-42px -4px}div.cartodb-timeslider p{width:120px;height:40px;margin:0;padding:0 5px 0 0;line-height:40px;font-size:13px;font-weight:700;font-family:Helvetica,Arial;text-align:center;color:#999}.cartodb-header{display:none;position:relative;width:100%;background-color:rgba(0,0,0,.5);font-family:'Helvetica Neue',Helvetica,sans-serif;line-height:normal;z-index:99999}.cartodb-header .content{padding:10px}.cartodb-header .content a{color:#fff}.cartodb-header .content a:hover{color:#ccc}.cartodb-header .content .title{display:none;margin:0 0 5px;line-height:normal;font-family:'Helvetica Neue',Helvetica,sans-serif;font-weight:700;font-size:15px;color:#fff}.cartodb-header .content .description{display:none;font-family:'Helvetica Neue',Helvetica,sans-serif;line-height:normal;color:#fff;font-size:13px}.cartodb-overlay.overlay-annotation,.cartodb-overlay.overlay-text{position:absolute;display:none;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;font-size:20px;line-height:normal;color:#fff;-ms-word-break:break-word;word-break:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;hyphens:auto;z-index:11}.cartodb-overlay.overlay-annotation .content,.cartodb-overlay.overlay-text .content{padding:10px}.cartodb-overlay.overlay-text .text{font-size:20px;line-height:normal;color:#fff;-ms-word-break:break-word;word-break:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;hyphens:auto}.cartodb-overlay.overlay-annotation .text strong,.cartodb-overlay.overlay-text .text strong{font-weight:700}.cartodb-overlay.overlay-annotation .text em,.cartodb-overlay.overlay-text .text em{font-style:italic}.cartodb-overlay.overlay-annotation div.text a,.cartodb-overlay.overlay-text div.text a{color:inherit}.cartodb-overlay.overlay-annotation .text a:hover,.cartodb-overlay.overlay-text .text a:hover{color:inherit;filter:alpha(Opacity=80);opacity:.8}.cartodb-overlay.overlay-annotation{-webkit-border-radius:2px;-moz-border-radius:2px;-ms-border-radius:2px;-o-border-radius:2px;border-radius:2px}.cartodb-overlay.overlay-annotation .content{padding:5px}.cartodb-overlay.overlay-annotation.align-right .stick .ball{left:auto;right:-6px}.cartodb-overlay.overlay-annotation .stick{position:absolute;top:50%;left:-50px;margin-top:-1px;width:50px;height:2px;background:#333}.cartodb-overlay.overlay-annotation .stick .ball{position:absolute;left:-6px;top:50%;margin-top:-3px;width:6px;height:6px;background:#333;-webkit-border-radius:200px;-moz-border-radius:200px;-ms-border-radius:200px;-o-border-radius:200px;border-radius:200px}.cartodb-overlay.image-overlay{display:none;position:absolute;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;z-index:11}.cartodb-overlay.image-overlay .content{padding:10px}.cartodb-overlay.image-overlay img{display:block}@font-face{font-family:'Droid Sans';font-style:normal;font-weight:400;src:local("Droid Sans"),local("DroidSans"),url(//themes.googleusercontent.com/static/fonts/droidsans/v4/s-BiyweUPV0v-yRb-cjciL3hpw3pgy2gAi-Ip7WPMi0.woff) format("woff")}@font-face{font-family:'Droid Sans';font-style:bold;font-weight:700;src:local("Droid Sans Bold"),local("DroidSans-Bold"),url(//themes.googleusercontent.com/static/fonts/droidsans/v4/EFpQQyG9GqCrobXxL-KRMXbFhgvWbfSbdVg11QabG8w.woff) format("woff")}@font-face{font-family:Vollkorn;font-style:normal;font-weight:400;src:local("Vollkorn Regular"),local("Vollkorn-Regular"),url(//themes.googleusercontent.com/static/fonts/vollkorn/v4/BCFBp4rt5gxxFrX6F12DKnYhjbSpvc47ee6xR_80Hnw.woff) format("woff")}@font-face{font-family:Vollkorn;font-style:normal;font-weight:400;src:local("Vollkorn Regular"),local("Vollkorn-Regular"),url(//themes.googleusercontent.com/static/fonts/vollkorn/v4/BCFBp4rt5gxxFrX6F12DKnYhjbSpvc47ee6xR_80Hnw.woff) format("woff")}@font-face{font-family:Vollkorn;font-style:bold;font-weight:700;src:local("Vollkorn Bold"),local("Vollkorn-Bold"),url(//themes.googleusercontent.com/static/fonts/vollkorn/v4/wMZpbUtcCo9GUabw9JODerrIa-7acMAeDBVuclsi6Gc.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:bold;font-weight:400;src:local("Open Sans"),local("OpenSans"),url(//themes.googleusercontent.com/static/fonts/opensans/v8/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:bold;font-weight:600;src:local("Open Sans Semibold"),local("OpenSans-Semibold"),url(//themes.googleusercontent.com/static/fonts/opensans/v8/MTP_ySUJH_bn48VBG8sNSqRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Roboto Slab';font-style:normal;font-weight:400;src:local("Roboto Slab Regular"),local("RobotoSlab-Regular"),url(//themes.googleusercontent.com/static/fonts/robotoslab/v3/y7lebkjgREBJK96VQi37ZrrIa-7acMAeDBVuclsi6Gc.woff) format("woff")}@font-face{font-family:'Roboto Slab';font-style:bold;font-weight:700;src:local("Roboto Slab Bold"),local("RobotoSlab-Bold"),url(//themes.googleusercontent.com/static/fonts/robotoslab/v3/dazS1PrQQuCxC3iOAJFEJRbnBKKEOwRKgsHDreGcocg.woff) format("woff")}@font-face{font-family:Lato;font-style:normal;font-weight:400;src:local("Lato Regular"),local("Lato-Regular"),url(//fonts.gstatic.com/s/lato/v11/8qcEw_nrk_5HEcCpYdJu8BTbgVql8nDJpwnrE27mub0.woff2) format("woff2");unicode-range:U+0100-024F,U+1E00-1EFF,U+20A0-20AB,U+20AD-20CF,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lato;font-style:normal;font-weight:400;src:local("Lato Regular"),local("Lato-Regular"),url(//fonts.gstatic.com/s/lato/v11/MDadn8DQ_3oT6kvnUq_2rxTbgVql8nDJpwnrE27mub0.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:Lato;font-style:normal;font-weight:700;src:local("Lato Bold"),local("Lato-Bold"),url(//fonts.gstatic.com/s/lato/v11/rZPI2gHXi8zxUjnybc2ZQFKPGs1ZzpMvnHX-7fPOuAc.woff2) format("woff2");unicode-range:U+0100-024F,U+1E00-1EFF,U+20A0-20AB,U+20AD-20CF,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lato;font-style:normal;font-weight:700;src:local("Lato Bold"),local("Lato-Bold"),url(//fonts.gstatic.com/s/lato/v11/MgNNr5y1C_tIEuLEmicLm1KPGs1ZzpMvnHX-7fPOuAc.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:Lato;font-style:italic;font-weight:400;src:local("Lato Italic"),local("Lato-Italic"),url(//fonts.gstatic.com/s/lato/v11/cT2GN3KRBUX69GVJ2b2hxn-_kf6ByYO6CLYdB4HQE-Y.woff2) format("woff2");unicode-range:U+0100-024F,U+1E00-1EFF,U+20A0-20AB,U+20AD-20CF,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lato;font-style:italic;font-weight:400;src:local("Lato Italic"),local("Lato-Italic"),url(//fonts.gstatic.com/s/lato/v11/1KWMyx7m-L0fkQGwYhWwun-_kf6ByYO6CLYdB4HQE-Y.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:Lato;font-style:italic;font-weight:700;src:local("Lato Bold Italic"),local("Lato-BoldItalic"),url(//fonts.gstatic.com/s/lato/v11/AcvTq8Q0lyKKNxRlL28Rn4X0hVgzZQUfRDuZrPvH3D8.woff2) format("woff2");unicode-range:U+0100-024F,U+1E00-1EFF,U+20A0-20AB,U+20AD-20CF,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lato;font-style:italic;font-weight:700;src:local("Lato Bold Italic"),local("Lato-BoldItalic"),url(//fonts.gstatic.com/s/lato/v11/HkF_qI1x_noxlxhrhMQYEIX0hVgzZQUfRDuZrPvH3D8.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:Graduate;font-style:normal;font-weight:400;src:local("Graduate"),local("Graduate-Regular"),url(//fonts.gstatic.com/s/graduate/v4/xBquLOzic3rRbJsTs3BiEBkAz4rYn47Zy2rvigWQf6w.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:'Old Standard TT';font-style:normal;font-weight:400;src:local("Old Standard TT Regular"),local("OldStandardTT-Regular"),url(//fonts.gstatic.com/s/oldstandardtt/v7/n6RTCDcIPWSE8UNBa4k-DLF-2NVkvf-rOuDmUqmzvVM.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:'Old Standard TT';font-style:normal;font-weight:700;src:local("Old Standard TT Bold"),local("OldStandardTT-Bold"),url(//fonts.gstatic.com/s/oldstandardtt/v7/5Ywdce7XEbTSbxs__4X1_C-wBZwrdXnFg8S-xRZijWL3rGVtsTkPsbDajuO5ueQw.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:'Old Standard TT';font-style:italic;font-weight:400;src:local("Old Standard TT Italic"),local("OldStandardTT-Italic"),url(//fonts.gstatic.com/s/oldstandardtt/v7/QQT_AUSp4AV4dpJfIN7U5L2K6DRqiD5gep8WjK7yGlo.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:'Gravitas One';font-style:normal;font-weight:400;src:local("Gravitas One"),local("GravitasOne"),url(//fonts.gstatic.com/s/gravitasone/v6/nBHdBv6zVNU8MtP6w9FwTRVuXpl7XtNjpLlhhhGlVqc.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}.cartodb-overlay.overlay-annotation .content>.text,.cartodb-overlay.overlay-text .content>.text{font-family:'Helvetica Neue',Helvetica,sans-serif;font-weight:400}.cartodb-overlay.overlay-annotation .content>.text strong,.cartodb-overlay.overlay-text .content>.text strong{font-family:'Helvetica Neue',Helvetica,sans-serif;font-weight:700}.cartodb-overlay.overlay-annotation.droid .content>.text,.cartodb-overlay.overlay-text.droid .content>.text{font-family:'Droid Sans',serif;font-weight:400}.cartodb-overlay.overlay-annotation.droid .content>.text strong,.cartodb-overlay.overlay-text.droid .content>.text strong{font-family:'Droid Sans',Helvetica,sans-serif;font-weight:700}.cartodb-overlay.overlay-annotation.roboto .content>.text,.cartodb-overlay.overlay-text.roboto .content>.text{font-family:'Roboto Slab',serif;font-weight:400}.cartodb-overlay.overlay-annotation.roboto .content>.text strong,.cartodb-overlay.overlay-text.roboto .content>.text strong{font-family:'Roboto Slab',serif;font-weight:700}.cartodb-overlay.overlay-annotation.vollkorn .content>.text,.cartodb-overlay.overlay-text.vollkorn .content>.text{font-family:Vollkorn,serif;font-weight:400}.cartodb-overlay.overlay-annotation.vollkorn .content>.text strong,.cartodb-overlay.overlay-text.vollkorn .content>.text strong{font-family:Vollkorn,serif;font-weight:700}.cartodb-overlay.overlay-annotation.open_sans .content>.text,.cartodb-overlay.overlay-text.open_sans .content>.text{font-family:'Open Sans',sans-serif;font-weight:400}.cartodb-overlay.overlay-annotation.open_sans .content>.text strong,.cartodb-overlay.overlay-text.open_sans .content>.text strong{font-family:'Open Sans',sans-serif;font-weight:700}.cartodb-overlay.overlay-annotation.lato .content>.text,.cartodb-overlay.overlay-text.lato .content>.text{font-family:Lato,sans-serif;font-weight:400}.cartodb-overlay.overlay-annotation.lato .content>.text strong,.cartodb-overlay.overlay-text.lato .content>.text strong{font-family:Lato,sans-serif;font-weight:700}.cartodb-overlay.overlay-annotation.graduate .content>.text,.cartodb-overlay.overlay-annotation.graduate .content>.text strong,.cartodb-overlay.overlay-text.graduate .content>.text,.cartodb-overlay.overlay-text.graduate .content>.text strong{font-family:Graduate,sans-serif;font-weight:400}.cartodb-overlay.overlay-annotation.old_standard_tt .content>.text,.cartodb-overlay.overlay-text.old_standard_tt .content>.text{font-family:'Old Standard TT',sans-serif;font-weight:400}.cartodb-overlay.overlay-annotation.old_standard_tt .content>.text strong,.cartodb-overlay.overlay-text.old_standard_tt .content>.text strong{font-family:'Old Standard TT',sans-serif;font-weight:700}.cartodb-overlay.overlay-annotation.gravitas_one .content>.text,.cartodb-overlay.overlay-annotation.gravitas_one .content>.text strong,.cartodb-overlay.overlay-text.gravitas_one .content>.text,.cartodb-overlay.overlay-text.gravitas_one .content>.text strong{font-family:'Gravitas One',sans-serif;font-weight:400}.cartodb-header .cartodb-slides-controller{background:0 0}.cartodb-slides-controller{position:relative;width:100%;text-align:center;top:0;left:0;background:rgba(0,0,0,.5);line-height:0;z-index:1000000}.cartodb-slides-controller .slides-controller-content{margin:auto;padding:10px}.cartodb-slides-controller .slides-controller-content .next,.cartodb-slides-controller .slides-controller-content .prev{position:relative}.cartodb-slides-controller .slides-controller-content .prev{display:inline-block;*display:inline;vertical-align:middle;width:16px;height:15px;margin:0 30px 0 0;background:url(../img/slide_left.png) no-repeat;border-radius:100px;opacity:.5}.cartodb-slides-controller .slides-controller-content .next{display:inline-block;*display:inline;vertical-align:middle;margin:0 0 0 30px;width:16px;height:15px;background:url(../img/slide_right.png) no-repeat;border-radius:100px;opacity:.5}.cartodb-slides-controller .slides-controller-content .next:hover,.cartodb-slides-controller .slides-controller-content .prev:hover{opacity:.8}.cartodb-slides-controller .slides-controller-content .prev:after{content:'';position:absolute;top:-5px;left:31px;height:25px;width:2px;background:#fff;opacity:.5}.cartodb-slides-controller .slides-controller-content .next:before{content:'';position:absolute;top:-5px;left:-17px;height:25px;width:2px;background:#fff;opacity:.5}.cartodb-slides-controller .slides-controller-content .counter{color:#fff}.cartodb-slides-controller .slides-controller-content .counter,.cartodb-slides-controller .slides-controller-content ul{display:inline-block;*display:inline;text-align:center;padding:0}.cartodb-slides-controller .slides-controller-content .counter.loading{opacity:.2;animation:loading .35s infinite ease-out alternate;-ms-animation:loading .35s infinite ease-out alternate;-moz-animation:loading .35s infinite ease-out alternate;-webkit-animation:loading .35s infinite ease-out alternate}.cartodb-slides-controller .slides-controller-content ul li{display:inline-block;*display:inline;vertical-align:middle;margin:0 2px}.cartodb-slides-controller .slides-controller-content ul li a{width:10px;height:10px;display:block;background:#fff;border-radius:100px;opacity:.4}.cartodb-slides-controller .slides-controller-content ul li a.active{opacity:1}.cartodb-slides-controller .slides-controller-content ul li a.active.time{width:10px;height:10px;opacity:.5;transform:scale(0.5);-ms-transform:scale(0.5);-moz-transform:scale(0.5);-webkit-transform:scale(0.5);animation:pulse .35s infinite ease-out alternate;-ms-animation:pulse .35s infinite ease-out alternate;-moz-animation:pulse .35s infinite ease-out alternate;-webkit-animation:pulse .35s infinite ease-out alternate}div.cartodb-timeslider .slider-wrapper{display:inline-block;zoom:1;*display:inline;vertical-align:top;width:253px;height:4px;_height:4px;padding:18px 15px}div.cartodb-timeslider .slider{width:253px;height:4px}div.cartodb-timeslider .ui-helper-hidden{display:none}div.cartodb-timeslider .ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}div.cartodb-timeslider .ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}div.cartodb-timeslider .ui-helper-clearfix:after,div.cartodb-timeslider .ui-helper-clearfix:before{content:"";display:table;border-collapse:collapse}div.cartodb-timeslider .ui-helper-clearfix:after{clear:both}div.cartodb-timeslider .ui-helper-clearfix{min-height:0}div.cartodb-timeslider .ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}div.cartodb-timeslider .ui-front{z-index:100}div.cartodb-timeslider .ui-state-disabled{cursor:default!important}div.cartodb-timeslider .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}div.cartodb-timeslider .ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}div.cartodb-timeslider .ui-slider{background-color:#E0E0E0;position:relative;text-align:left;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;-o-border-radius:2px}div.cartodb-timeslider .ui-slider .ui-slider-handle{position:absolute;z-index:102;width:9px;height:10px;cursor:default;background:url(../img/slider.png) no-repeat -98px -18px #fff;border:1px solid #555;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;-o-border-radius:2px;outline:0}div.cartodb-timeslider .ui-slider .ui-slider-handle:hover{cursor:col-resize;background-position:-112px -18px}div.cartodb-timeslider .ui-slider .ui-slider-range{position:absolute;z-index:100;font-size:.7em;display:block;border:0;background-position:0 0;background-color:#397DBA;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;-o-border-radius:2px}div.cartodb-timeslider .ui-slider.ui-state-disabled .ui-slider-handle,div.cartodb-timeslider .ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}div.cartodb-timeslider .ui-slider-horizontal{height:4px;cursor:pointer}div.cartodb-timeslider .ui-slider-horizontal .ui-slider-handle{top:-4px;margin-left:-6px}div.cartodb-timeslider .ui-slider-horizontal .ui-slider-range{top:0;height:100%;cursor:pointer}div.cartodb-timeslider .ui-slider-horizontal .ui-slider-range-min{left:0}div.cartodb-timeslider .ui-slider-horizontal .ui-slider-range-max{right:0}div.cartodb-timeslider .ui-slider-vertical{width:.8em;height:100px}div.cartodb-timeslider .ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}div.cartodb-timeslider .ui-slider-vertical .ui-slider-range{left:0;width:100%}div.cartodb-timeslider .ui-slider-vertical .ui-slider-range-min{bottom:0}div.cartodb-timeslider .ui-slider-vertical .ui-slider-range-max{top:0}@media only screen and (min-width:360px) and (max-width:500px){div.cartodb-timeslider .slider,div.cartodb-timeslider .slider-wrapper{width:130px}}@media only screen and (min-width:180px) and (max-width:360px){div.cartodb-timeslider .slider,div.cartodb-timeslider .slider-wrapper{width:90px}div.cartodb-timeslider p.value{width:90px;font-size:12px}}div.cartodb-tooltip-content-wrapper.dark{background:#000;background:rgba(0,0,0,.75);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#bf000000, endColorstr=#bf000000);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#bf000000, endColorstr=#bf000000)"}div.cartodb-tooltip-content-wrapper.dark h4{color:#999}div.cartodb-tooltip-content-wrapper.dark p{color:#FFF}div.cartodb-tooltip-content-wrapper.dark a{color:#397DB9}div.cartodb-tooltip{position:absolute;display:none;min-width:120px;max-width:180px;overflow-y:hidden;z-index:50}div.cartodb-tooltip-content-wrapper{-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;background:#fff;background:rgba(255,255,255,.9);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#E5FFFFFF, endColorstr=#E5FFFFFF);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#E5FFFFFF, endColorstr=#E5FFFFFF)";zoom:1}div.cartodb-tooltip-content{display:block;padding:8px 8px 8px 9px}div.cartodb-tooltip-content h4{display:block;margin:0 0 1px;text-transform:uppercase;font:400 10px "Helvetica Neue",Helvetica,Arial;color:#AAA;word-wrap:break-word}div.cartodb-tooltip-content p{display:block;margin:0 0 4px;padding:0 0 7px;font:400 12px "Helvetica Neue",Helvetica,Arial;color:#333;word-wrap:break-word}div.cartodb-tooltip-content p:last-child{padding:0;margin:0}div.cartodb-tooltip-content a{color:#0078A8}div.cartodb-tooltip>p{font-family:robotoregular,Helvetica,Arial,Sans-serif;font-size:15px;color:#333;text-align:center;text-shadow:-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff,1px 1px 0 #fff}.u-tSpace{margin-top:4px}.u-rSpace{margin-right:4px}.leaflet-image-layer,.leaflet-layer,.leaflet-map-pane,.leaflet-marker-icon,.leaflet-marker-pane,.leaflet-marker-shadow,.leaflet-overlay-pane,.leaflet-overlay-pane svg,.leaflet-popup-pane,.leaflet-shadow-pane,.leaflet-tile,.leaflet-tile-container,.leaflet-tile-pane,.leaflet-zoom-box{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden;-ms-touch-action:none}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container img{max-width:none!important}.leaflet-container img.leaflet-image-layer{max-width:15000px!important}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-tile-pane{z-index:2}.leaflet-objects-pane{z-index:3}.leaflet-overlay-pane{z-index:4}.leaflet-shadow-pane{z-index:5}.leaflet-marker-pane{z-index:6}.leaflet-popup-pane{z-index:7}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:7;pointer-events:auto}.leaflet-bottom,.leaflet-top{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup,.leaflet-fade-anim .leaflet-tile{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;-o-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup,.leaflet-fade-anim .leaflet-tile-loaded{opacity:1}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);-o-transition:-o-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-pan-anim .leaflet-tile,.leaflet-touching .leaflet-zoom-animated,.leaflet-zoom-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-clickable{cursor:pointer}.leaflet-container{cursor:-webkit-grab;cursor:-moz-grab}.leaflet-control,.leaflet-popup-pane{cursor:auto}.leaflet-dragging .leaflet-clickable,.leaflet-dragging .leaflet-container{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing}.leaflet-container{background:#ddd;outline:0}.leaflet-container a{color:#0078A8}.leaflet-container a.leaflet-active{outline:2px solid orange}.leaflet-zoom-box{border:2px dotted #38f;background:rgba(255,255,255,.5)}.leaflet-container{font:12px/1.5 "Helvetica Neue",Arial,Helvetica,sans-serif}.leaflet-bar{box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:4px}.leaflet-bar a,.leaflet-bar a:hover{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:0}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px "Lucida Console",Monaco,monospace;text-indent:1px}.leaflet-control-zoom-out{font-size:20px}.leaflet-touch .leaflet-control-zoom-in{font-size:22px}.leaflet-touch .leaflet-control-zoom-out{font-size:24px}.leaflet-control-layers{box-shadow:0 1px 5px rgba(0,0,0,.4);background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(../../img/layers.png);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(../../img/layers-2x.png);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-container .leaflet-control-attribution{background:#fff;background:rgba(255,255,255,.7);margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover{text-decoration:underline}.leaflet-container .leaflet-control-attribution,.leaflet-container .leaflet-control-scale{font-size:11px}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:0;line-height:1.1;padding:2px 5px 1px;font-size:11px;white-space:nowrap;overflow:hidden;-moz-box-sizing:content-box;box-sizing:content-box;background:#fff;background:rgba(255,255,255,.5)}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:0;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers{box-shadow:none}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-layers{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 19px;line-height:1.4}.leaflet-popup-content p{margin:18px 0}.leaflet-popup-tip-container{margin:0 auto;width:40px;height:20px;position:relative;overflow:hidden}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;box-shadow:0 3px 14px rgba(0,0,0,.4)}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;padding:4px 4px 0 0;text-align:center;width:18px;height:14px;font:16px/14px Tahoma,Verdana,sans-serif;color:#c3c3c3;text-decoration:none;font-weight:700;background:0 0}.leaflet-container a.leaflet-popup-close-button:hover{color:#999}.leaflet-popup-scrolled{overflow:auto;border-bottom:1px solid #ddd;border-top:1px solid #ddd}.leaflet-oldie .leaflet-popup-content-wrapper{zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678, M12=.70710678, M21=-.70710678, M22=.70710678)}.leaflet-oldie .leaflet-popup-tip-container{margin-top:-1px}.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.Widget-button{-webkit-transition:background,250ms;-khtml-transition:background,250ms;-moz-transition:background,250ms;-ms-transition:background,250ms;-o-transition:background,250ms;transition:background,250ms;display:inline-block;padding:0 26px;border-radius:4px;font-family:'Open Sans';line-height:40px}.Widget-link{font-size:10px;font-weight:500;text-transform:uppercase}.Widget-link:hover{text-decoration:underline}.Widget-button:hover,.Widget-link:hover{cursor:pointer}.Widget-canvas{position:absolute;top:0;right:0;bottom:0;width:352px;height:100%}@media only screen and (min-width:320px) and (max-width:760px){.Widget-canvas{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;-moz-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;position:relative;width:100%;height:auto;overflow-x:auto;overflow-y:visible}}.Widget-content{position:relative}.Tooltip{top:0;left:0;display:none;position:absolute;color:#fff;padding:5px;min-width:50px;box-sizing:border-box;text-align:center;border-radius:3px;background:#000;font-size:10px;pointer-events:none}.Chart.is-selectable .Handle,.Chart.is-selectable .HandleLine{opacity:1}.Widget{position:relative}.Widget-filter{opacity:0}.extent{opacity:0;fill-opacity:0;stroke:#3AA9E3;shape-rendering:crispEdges}.Bar{fill:#9DE0AD;transition:fill 200ms ease;shape-rendering:crispEdges}.Bar.is-highlighted{fill:#5BA45E;opacity:1}.Bar.is-selected{fill:#DDD;opacity:.7}.mini .Bar,.mini .Bar.is-highlighted{fill:#333}.mini .Bar.is-selected{fill:#DDD}.axis{font:10px sans-serif;shape-rendering:crispEdges}.axis path{display:none;shape-rendering:crispEdges}.axis line{fill:none;stroke-width:1;opacity:.2;stroke:#000;shape-rendering:crispEdges}.y{stroke:#EEE;stroke-width:1;shape-rendering:crispEdges}.l_bottom{stroke-width:1;opacity:.2;stroke:#000;shape-rendering:crispEdges}.Line{stroke-width:2;stroke:#3AA9E3}.Handle{opacity:0;stroke-width:2;stroke:#3AA9E3;fill:#FFF}.HandleLine{opacity:0;stroke-width:2;stroke:#3AA9E3}.Widget-filterButton.is-hidden{display:none}.Widget{width:352px;padding:22px 0 24px;box-sizing:border-box;border-bottom:2px solid transparent;font-family:'Open Sans'}.Widget-title{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:flex-start;-moz-justify-content:flex-start;-ms-justify-content:flex-start;justify-content:flex-start;-ms-flex-pack:start;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;-ms-flex-align:center;width:100%;height:22px;margin:0 0 2px}.Widget-content,.Widget-footer,.Widget-header{margin-right:24px;margin-left:24px}.Widget-content{display:block;margin-top:12px}.Widget-content--noSidesMargin{margin:12px 0 0}@media only screen and (min-width:320px) and (max-width:760px){.Widget{width:280px;border-right:2px solid transparent;border-bottom:0}}.Widget-error{-webkit-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;justify-content:center;-ms-flex-pack:center;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;-ms-flex-align:center;opacity:0;filter:alpha(opacity=0);-webkit-transition:opacity,500ms;-khtml-transition:opacity,500ms;-moz-transition:opacity,500ms;-ms-transition:opacity,500ms;-o-transition:opacity,500ms;transition:opacity,500ms;display:none;position:absolute;top:0;right:0;bottom:0;left:0;border-top:2px solid transparent;z-index:11}.Widget-error.is-visible{opacity:1;filter:alpha(opacity=100)}.Widget-filter{width:100%;margin:5px 0}.Widget-filterButtons{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;justify-content:space-between;-ms-flex-pack:justify;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;-ms-flex-align:center}.Widget-filterButton{margin-right:8px}.Widget-filterButton:last-child{margin-right:0}.Widget-info{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:flex-start;-moz-justify-content:flex-start;-ms-justify-content:flex-start;justify-content:flex-start;-ms-flex-pack:start;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;-ms-flex-align:center;width:100%}.Widget-infoItem{margin-right:12px}.Widget-infoItem:last-child{margin-right:0}@media only screen and (min-width:320px) and (max-width:760px){.Widget-info{-webkit-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;justify-content:space-between;-ms-flex-pack:justify}.Widget-infoItem{margin-right:0}}.Widget-contentSpaced{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;justify-content:space-between;-ms-flex-pack:justify;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;-ms-flex-align:center}.Widget-contentSpaced--topAligned{-webkit-align-items:flex-start;-moz-align-items:flex-start;-ms-align-items:flex-start;align-items:flex-start;-ms-flex-align:flex-start}.Widget-contentSpaced--start{-webkit-justify-content:flex-start;-moz-justify-content:flex-start;-ms-justify-content:flex-start;justify-content:flex-start;-ms-flex-pack:start;text-align:left}.Widget-contentFull{width:100%}.Widget-listWrapper{position:relative}.Widget-listEdge{display:block;position:absolute;right:0;left:0;height:1px;z-index:2}.Widget-listEdgeShadow{position:absolute;right:0;left:0;width:100%;height:35px;z-index:0;pointer-events:none}.Widget-listEdgeBorder{position:absolute;right:24px;left:24px;height:1px;z-index:1}.Widget-listEdge--top,.Widget-listEdge--top .Widget-listEdgeShadow{top:0}.Widget-listEdge--bottom{bottom:0}.Widget-listEdge--bottom .Widget-listEdgeShadow{-webkit-transform:rotate(180deg);-khtml-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg);bottom:0}.Widget-list{display:block;position:relative;max-height:232px;margin:0;overflow:hidden;z-index:1}.Widget-listItem{display:block}.Widget-listItemInner{margin:0 24px;padding:5px 0}.Widget-listItemInner--withBorders{padding:7px 0}.Widget-listItem--fake{position:relative;height:40px;margin:8px 24px;border-bottom:1px solid transparent}.Widget-listItem--fake:after,.Widget-listItem--fake:before{display:inline-block;position:relative;margin:0;padding:0;content:''}.Widget-listItem--fake:before{width:60px;height:8px}.Widget-listItem--fake:after{width:100%;height:2px}.Widget-listSubItem{display:inline-block}.Widget-listDot{margin-top:3px;margin-right:5px}.Widget-listButton{position:relative;width:100%;margin:0;padding:5px 24px;font-family:'Open Sans';font-weight:500}.Widget-listButton:hover{cursor:pointer}.Widget-listButton--withBorder{padding:7px 24px}.Widget-listButton--withBorder:after,.Widget-listButton--withBorder:before{position:absolute;right:24px;left:24px;height:1px;content:''}.Widget-listButton--withBorder:before{top:-1px}.Widget-listButton--withBorder:after{bottom:0}.Widget-listButton--withBorder:hover:after,.Widget-listButton--withBorder:hover:before{right:0;left:0}.Widget-inlineList{width:100%;padding:0}.Widget-inlineListItem{display:inline-block;width:32%;margin:5px 0 0;vertical-align:top}@media only screen and (min-width:320px) and (max-width:760px){.Widget-inlineListItem{width:47%}}.Widget-loader{-webkit-animation:loader-progress 1000ms linear 1;-khtml-animation:loader-progress 1000ms linear 1;-moz-animation:loader-progress 1000ms linear 1;-ms-animation:loader-progress 1000ms linear 1;-o-animation:loader-progress 1000ms linear 1;animation:loader-progress 1000ms linear 1;opacity:0;filter:alpha(opacity=0);-webkit-transition:opacity,1000ms;-khtml-transition:opacity,1000ms;-moz-transition:opacity,1000ms;-ms-transition:opacity,1000ms;-o-transition:opacity,1000ms;transition:opacity,1000ms;position:absolute;top:0;left:0;width:100%;height:2px;z-index:10}.Widget-loader.is-visible{-webkit-animation:loader-progress 2000ms linear infinite;-khtml-animation:loader-progress 2000ms linear infinite;-moz-animation:loader-progress 2000ms linear infinite;-ms-animation:loader-progress 2000ms linear infinite;-o-animation:loader-progress 2000ms linear infinite;animation:loader-progress 2000ms linear infinite;opacity:1;filter:alpha(opacity=100)}@-webkit-keyframes loader-progress{0%{width:0}100%{width:100%}}@-moz-keyframes loader-progress{0%{width:0}100%{width:100%}}@-ms-keyframes loader-progress{0%{width:0}100%{width:100%}}@-o-keyframes loader-progress{0%{width:0}100%{width:100%}}@keyframes loader-progress{0%{width:0}100%{width:100%}}.Widget-nav{width:100%;margin-top:12px}.Widget-navDots{height:8px}.Widget-dot--navigation{display:inline-block;vertical-align:top}.Widget-dot--navigation:hover{cursor:pointer}.Widget-navDotsItem.is-selected:hover{cursor:default}.Widget-navDotsItem.is-disabled{opacity:.5;filter:alpha(opacity=50)}.Widget-navArrows{width:45px;height:9px}.Widget-progressBar{position:relative;width:100%;height:4px;margin:3px 0;border-radius:4px}.Widget-progressState{position:absolute;top:0;left:0;max-width:100%;height:4px;border-radius:4px}.Widget-progressState--positive{left:0;border-top-right-radius:0;border-bottom-right-radius:0}.Widget-progressState--negative{right:0;left:auto;border-top-left-radius:0;border-bottom-left-radius:0}.Widget-dot{display:inline-block;width:8px;min-width:8px;height:8px;border-radius:8px}.Widget-arrow{display:inline-block;position:relative;width:16px;height:10px}.Widget-arrow:hover{cursor:pointer}.Widget-arrow:after,.Widget-arrow:before{position:absolute;top:5px;width:10px;height:1px;content:''}.Widget-arrow.is-disabled{opacity:.5;filter:alpha(opacity=50)}.Widget-arrow.is-disabled:hover{cursor:default}.Widget-arrow--up:after{-webkit-transform:rotate(45deg);-khtml-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg);right:0}.Widget-arrow--up:before{-webkit-transform:rotate(-45deg);-khtml-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);left:0}.Widget-arrow--down:after{-webkit-transform:rotate(-45deg);-khtml-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);right:0}.Widget-arrow--down:before{-webkit-transform:rotate(45deg);-khtml-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg);left:0}.Widget-lens{display:inline-block;position:relative;width:16px;height:16px}.Widget-lens:after,.Widget-lens:before{position:absolute;content:''}.Widget-lens:after{top:0;left:0;width:8px;height:8px;border:1px solid transparent;border-radius:10px}.Widget-lens:before{-webkit-transform:rotate(45deg);-khtml-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg);right:3px;bottom:5px;width:6px;height:1px}.Widget-lens:hover{cursor:pointer}.Widget-tag{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;-ms-flex-align:center;height:22px;margin:0 0 0 12px;padding:0 6px;border-radius:4px;vertical-align:middle}.Widget-tag--green{background:#EDF3DF}.Widget-tag--blue{background:#DFF2FC}.Widget-textBigger{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:26px;font-weight:500;line-height:41.6px}.Widget-textBig{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;font-size:16px;font-weight:500;line-height:20px}.Widget-textNormal{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:14px;font-weight:500;line-height:501}.Widget-textNormal--bold{font-weight:600}.Widget-textSmall{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12px;font-weight:500;line-height:13px}.Widget-textSmall--bold{font-weight:600}.Widget-textSmall--upper{text-transform:uppercase}.Widget-textSmaller{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:10px;font-weight:500;line-height:13px}.Widget-textSmaller--bold{font-weight:600}.Widget-textSmaller--upper{text-transform:uppercase}.Widget-textSmaller--noEllip{text-overflow:inherit;white-space:inherit;overflow:inherit}.Widget--light{border-color:#f2f6f9;background:#fff}.Widget--light .Widget-textBig,.Widget--light .Widget-textNormal,.Widget--light .Widget-textSmall{color:#2e3c43}.Widget--light .Widget-textSmaller{color:#636d72}.Widget--light .Widget-textSmaller--dark{color:#2e3c43}.Widget--light .Widget-link{color:#3aa9e3}.Widget--light .Widget-link:hover{color:#227dbd}.Widget--light .Widget-progressBar{background:#eee}.Widget--light .Widget-progressState{background:#9de0ad}.Widget--light .Widget-progressState--positive{background:#8fb83f}.Widget--light .Widget-progressState--negative{background:#f15743}.Widget--light .Widget-dot{background:#9de0ad}.Widget--light .Widget-dot--navigation{background:#eee}.Widget--light .Widget-dot--navigation:hover{background:#aaa}.Widget--light .Widget-dot--navigation.is-selected{background:#636d72}.Widget--light .Widget-listEdgeBorder{background:#eee}.Widget--light .Widget-listEdgeShadow{background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(rgba(255,255,255,0)));background-image:-webkit-linear-gradient(top,#fff,rgba(255,255,255,0));background-image:-moz-linear-gradient(top,#fff,rgba(255,255,255,0));background-image:-ms-linear-gradient(top,#fff,rgba(255,255,255,0));background-image:-o-linear-gradient(top,#fff,rgba(255,255,255,0));background-image:linear-gradient(top,#fff,rgba(255,255,255,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorStr='#fff', endColorStr='rgba(255,255,255,0)');background-color:transparent}.Widget--light .Widget-listItemInner--withBorders{border-bottom:1px solid #eee}.Widget--light .Widget-listItem--fake{border-color:#f9f9f9}.Widget--light .Widget-listItem--fake:after,.Widget--light .Widget-listItem--fake:before{background-color:#f9f9f9}.Widget--light .Widget-listButton:hover{background:#f2f6f9}.Widget--light .Widget-listButton--withBorder:before{background:0 0}.Widget--light .Widget-listButton--withBorder:after,.Widget--light .Widget-listButton--withBorder:hover:after,.Widget--light .Widget-listButton--withBorder:hover:before{background:#eee}.Widget--light .Widget-loader{background:#3aa9e3}.Widget--light .Widget-error{border-color:#f15743;background:rgba(242,246,249,.8)}.Widget--light .Widget-errorButton{background:#636d72}.Widget--light .Widget-errorButton .Widget-textSmall,.Widget--light .Widget-errorButton .Widget-textSmaller{color:#FFF}.Widget--light .Widget-errorButton:hover{background:#2e3c43}.Widget--light .Widget-arrow:after,.Widget--light .Widget-arrow:before{background-color:#3aa9e3}.Widget--light .Widget-arrow:hover:after,.Widget--light .Widget-arrow:hover:before{background-color:#227dbd}.Widget--light .Widget-lens:after{border-color:#3aa9e3}.Widget--light .Widget-lens:before{background:#3aa9e3}.Widget--light .Widget-lens:hover:after{border-color:#227dbd}.Widget--light .Widget-lens:hover:before{background:#227dbd}.Widget--light .is-disabled .Widget-textNormal,.Widget--light .is-disabled .Widget-textSmall,.Widget--light .is-disabled .Widget-textSmaller,.Widget--light .is-disabled .Widget-textSmaller--dark{color:#cbced0}.Widget--light .is-disabled .Widget-progressState{background:#cbced0!important} \ No newline at end of file diff --git a/examples/vendor/cartodb.uncompressed.js b/examples/vendor/cartodb.uncompressed.js new file mode 100644 index 00000000..929e23f1 --- /dev/null +++ b/examples/vendor/cartodb.uncompressed.js @@ -0,0 +1,55281 @@ +// cartodb.js version: 3.15.8 +// uncompressed version: cartodb.uncompressed.js +// sha: 3702ae15b7366d09fd91f350b93a99536c41a55f +(function() { + var define; // Undefine define (require.js), see https://github.com/CartoDB/cartodb.js/issues/543 + var root = this; + + if(!true) { + if(root.jQuery === undefined) { + throw "jQuery should be loaded before include cartodb.js"; + } + } + + // save current libraries + var __prev = { + jQuery: root.jQuery, + $: root.$, + L: root.L, + Mustache: root.Mustache, + Backbone: root.Backbone, + _: root._ + }; +/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ +return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n(""; + + this.dialog = new cdb.ui.common.ShareDialog({ + title: data.map.get("title"), + description: data.map.get("description"), + model: this.options.vis.map, + code: code, + url: data.url, + public_map_url: public_map_url, + share_url: data.share_url, + template: template, + target: $(".cartodb-share a"), + size: $(document).width() > 400 ? "" : "small", + width: $(document).width() > 400 ? 430 : 216 + }); + + $(".cartodb-map-wrapper").append(this.dialog.render().$el); + + this.addView(this.dialog); + + }, + + render: function() { + + this.$el.html(this.template(_.extend(this.model.attributes))); + + return this; + + } + +}); + +/** + * View to control the zoom of the map. + * + * Usage: + * + * var zoomControl = new cdb.geo.ui.Zoom({ model: map }); + * mapWrapper.$el.append(zoomControl.render().$el); + * + */ + + +cdb.geo.ui.Zoom = cdb.core.View.extend({ + + className: "cartodb-zoom", + + events: { + 'click .zoom_in': 'zoom_in', + 'click .zoom_out': 'zoom_out' + }, + + default_options: { + timeout: 0, + msg: '' + }, + + initialize: function() { + this.map = this.model; + + _.defaults(this.options, this.default_options); + + this.template = this.options.template ? this.options.template : cdb.templates.getTemplate('geo/zoom'); + this.map.bind('change:zoom change:minZoom change:maxZoom', this._checkZoom, this); + }, + + render: function() { + this.$el.html(this.template(this.options)); + this._checkZoom(); + return this; + }, + + _checkZoom: function() { + var zoom = this.map.get('zoom'); + this.$('.zoom_in')[ zoom < this.map.get('maxZoom') ? 'removeClass' : 'addClass' ]('disabled') + this.$('.zoom_out')[ zoom > this.map.get('minZoom') ? 'removeClass' : 'addClass' ]('disabled') + }, + + zoom_in: function(ev) { + if (this.map.get("maxZoom") > this.map.getZoom()) { + this.map.setZoom(this.map.getZoom() + 1); + } + ev.preventDefault(); + ev.stopPropagation(); + }, + + zoom_out: function(ev) { + if (this.map.get("minZoom") < this.map.getZoom()) { + this.map.setZoom(this.map.getZoom() - 1); + } + ev.preventDefault(); + ev.stopPropagation(); + } + +}); + +/** + * View to know which is the map zoom. + * + * Usage: + * + * var zoomInfo = new cdb.geo.ui.ZoomInfo({ model: map }); + * mapWrapper.$el.append(zoomInfo.render().$el); + * + */ + + +cdb.geo.ui.ZoomInfo = cdb.core.View.extend({ + + className: "cartodb-zoom-info", + + initialize: function() { + this.model.bind("change:zoom", this.render, this); + }, + + render: function() { + this.$el.html(this.model.get("zoom")); + return this; + } +}); + + +// MODELS & COLLECTIONS + +/* + * Model for the legend item + * + * */ +cdb.geo.ui.LegendItemModel = cdb.core.Model.extend({ + + defaults: { + name: "Untitled", + visible:true, + value: "" + } + +}); + +/* + * Collection of items for a legend + * + * */ +cdb.geo.ui.LegendItems = Backbone.Collection.extend({ + model: cdb.geo.ui.LegendItemModel +}); + + +/* + * Legend Model + * + **/ +cdb.geo.ui.LegendModel = cdb.core.Model.extend({ + + defaults: { + type: null, + show_title: false, + title: "", + template: "", + visible: true + }, + + initialize: function() { + + this.items = new cdb.geo.ui.LegendItems(this.get("items")); + + this.items.bind("add remove reset change", function() { + this.set({ items: this.items.toJSON() }); + }, this); + + this.bind("change:items", this._onUpdateItems, this); + this.bind("change:title change:show_title", this._onUpdateTitle, this); + this.bind("change:template", this._onUpdateTemplate, this); + + }, + + _onUpdateTemplate: function() { + this.template = this.get("template"); + }, + + _onUpdateTitle: function() { + this.title = this.get("title"); + this.show_title = this.get("show_title"); + }, + + _onUpdateItems: function() { + var items = this.get("items"); + this.items.reset(items); + } + +}); + +cdb.geo.ui.Legends = Backbone.Collection.extend({ + model: cdb.geo.ui.LegendModel +}); + +// VIEWS + +/* + * Legend item + * + * */ +cdb.geo.ui.LegendItem = cdb.core.View.extend({ + + tagName: "li", + + initialize: function() { + + _.bindAll(this, "render"); + + this.template = this.options.template ? _.template(this.options.template) : cdb.templates.getTemplate('geo/legend'); + + }, + + render: function() { + + var value; + this.model.attributes.name = ""+this.model.attributes.name; + if (this.model.get("type") == 'image' && this.model.get("value")) { + value = "url( " + this.model.get("value") + ")"; + } else { + value = this.model.get("value"); + } + + var options = _.extend( this.model.toJSON(), { value: value }); + + this.$el.html(this.template(options)); + + return this.$el; + } + +}); + +/* + * Legend View: wrapper for the different types of lengeds + * + * */ +cdb.geo.ui.Legend = cdb.core.View.extend({ + + className: "cartodb-legend", + + events: { + "dragstart": "_stopPropagation", + "mousedown": "_stopPropagation", + "touchstart": "_stopPropagation", + "MSPointerDown": "_stopPropagation", + "dblclick": "_stopPropagation", + "mousewheel": "_stopPropagation", + "DOMMouseScroll": "_stopPropagation", + "dbclick": "_stopPropagation", + "click": "_stopPropagation" + }, + + initialize: function() { + _.bindAll(this, "render", "show", "hide"); + + _.defaults(this.options, this.default_options); + + this.map = this.options.map; + + this._setupModel(); + this._setupItems(); + + this._updateLegendType(); + }, + + _stopPropagation: function(ev) { + ev.stopPropagation(); + }, + + _setupModel: function() { + if (!this.model) { + + this.model = new cdb.geo.ui.LegendModel({ + type: this.options.type || cdb.geo.ui.LegendModel.prototype.defaults.type, + title: this.options.title || cdb.geo.ui.LegendModel.prototype.defaults.title, + show_title: this.options.show_title || cdb.geo.ui.LegendModel.prototype.defaults.show_title, + template: this.options.template || cdb.geo.ui.LegendModel.prototype.defaults.template + }); + } + + this.add_related_model(this.model); + + //this.model.bind("change:template change:type change:items change:title change:show_title", this._updateLegendType, this); + this.model.bind("change", this._updateLegendType, this); + }, + + _updateLegendType: function() { + var type = this.model.get("type"); + this.legend_name = this._capitalize(type) + "Legend"; + + if (type == 'none' || type == null) { + this.legend_name = null; + this.model.set({ type: "none" }, { silent: true }); + } else if (!cdb.geo.ui[this.legend_name]) { + + // set the previous type + this.legend_name = null; + this.model.set({ type: this.model.previous("type") }, { silent: true }); + return; + } + + this._refresh(); + }, + + _capitalize: function(string) { + if (string && _.isString(string)) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + }, + + _refresh: function() { + var self = this; + + if (this.view) this.view.clean(); + + var type = this.model.get("type"); + var title = this.model.get("title"); + var show_title = this.model.get("show_title"); + var template = this.model.get("template"); + + if (type && this.legend_name) { + this.view = new cdb.geo.ui[this.legend_name]({ + model: this.model + }); + + // Set the type as the element class for styling + this.$el.removeClass(); + this.$el.addClass(this.className + " " + this.model.get("type")); + } else { + this.hide(); + + this.$el.removeClass(); + this.$el.addClass(this.className + " none"); + } + + this.render(); + }, + + _setupItems: function() { + var self = this; + + this.items = this.model.items; + + if (this.options.data) { + this.items.reset(this.options.data); + } + + this.items.bind("add remove change:value change:name", this.render, this); + }, + + render: function() { + if (this.view) { + + if (this.model.get("template")) { + this.$el.html(this.view.render().$el.html()); + this.$el.removeClass(this.model.get("type")) + this.$el.addClass("wrapper"); + } else { + this.$el.html(this.view.render().$el.html()); + } + + if (this.model.get("visible") === false) { + this.hide(); + } else { + this.show(); + } + } + + return this; + }, + + show: function(callback) { + var type = this.model.get("type"); + if (type && type != "none") this.$el.show(); + }, + + hide: function(callback) { + if (this.model.get("type")) this.$el.hide(); + } +}); + +/* + * DebugLegend + * + * */ +cdb.geo.ui.DebugLegend = cdb.core.View.extend({ }); + +/* + * BaseLegend: common methods for all the legends + * + * */ +cdb.geo.ui.BaseLegend = cdb.core.View.extend({ + + _bindModel: function() { + + this.model.bind("change:template change:title change:show_title", this.render, this); + + }, + + addTo: function(element) { + $(element).html(this.render().$el); + }, + + setTitle: function(title) { + this.model.set("title", title); + }, + + showTitle: function() { + this.model.set("show_title", true); + }, + + hideTitle: function() { + this.model.set("show_title", false); + } + +}); + +/* + * NoneLegend + * + * */ +cdb.geo.ui.NoneLegend = cdb.geo.ui.BaseLegend.extend({ }); +cdb.geo.ui.Legend.None = cdb.core.View.extend({ }); + +/* + * ChoroplethLegend + * + * */ +cdb.geo.ui.ChoroplethLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "choropleth-legend", + + template: _.template('<% if (title && show_title) { %>\n
<%- title %>
<% } %>
  • \t\t<%- leftLabel %>
  • \t\t<%- rightLabel %>
  • \t
    <%= colors %>\n\t
'), + + initialize: function() { + + this.items = this.model.items; + + }, + + _generateColorList: function() { + + var colors = ""; + + if (this.model.get("colors")) { + return _.map(this.model.get("colors"), function(color) { + return '\n\t
'; + }).join(""); + } else { + + for (var i = 2; i < this.items.length; i++) { + var color = this.items.at(i).get("value"); + colors += '\n\t
'; + } + } + + return colors; + + }, + + setLeftLabel: function(text) { + + this.model.set("leftLabel", text); + + }, + + setRightLabel: function(text) { + + this.model.set("rightLabel", text); + + }, + + setColors: function(colors) { + + this.model.set("colors", colors); + + }, + + render: function() { + + if (this.model.get("template")) { + + var template = _.template(cdb.core.sanitize.html(this.model.get("template"), this.model.get('sanitizeTemplate'))); + this.$el.html(template(this.model.toJSON())); + + } else { + + + if (this.items.length >= 2) { + + this.leftLabel = this.items.at(0); + this.rightLabel = this.items.at(1); + + var leftLabel = this.model.get("leftLabel") || this.leftLabel.get("value"); + var rightLabel = this.model.get("rightLabel") || this.rightLabel.get("value"); + + var colors = this._generateColorList(); + + var options = _.extend( this.model.toJSON(), { leftLabel: leftLabel, rightLabel: rightLabel, colors: colors, buckets_count: colors.length }); + + this.$el.html(this.template(options)); + } + } + + return this; + + } + +}); + +/* + * DensityLegend + * + * */ +cdb.geo.ui.DensityLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "density-legend", + + template: _.template('<% if (title && show_title) { %>\n
<%- title %>
<% } %>
  • \t<%- leftLabel %>
  • \t<%- rightLabel %>
  • \t
    <%= colors %>\n\t
'), + + initialize: function() { + + this.items = this.model.items; + + }, + + setLeftLabel: function(text) { + + this.model.set("leftLabel", text); + + }, + + setRightLabel: function(text) { + + this.model.set("rightLabel", text); + + }, + + setColors: function(colors) { + + this.model.set("colors", colors); + + }, + + _generateColorList: function() { + + var colors = ""; + + if (this.model.get("colors")) { + + return _.map(this.model.get("colors"), function(color) { + return '\n\t\t
'; + }).join(""); + + } else { + + for (var i = 2; i < this.items.length; i++) { + var color = this.items.at(i).get("value"); + colors += '\n\t\t
'; + } + } + + return colors; + + }, + + + render: function() { + + if (this.model.get("template")) { + + var template = _.template(cdb.core.sanitize.html(this.model.get("template"), this.model.get('sanitizeTemplate'))); + this.$el.html(template(this.model.toJSON())); + + } else { + + if (this.items.length >= 2) { + + this.leftLabel = this.items.at(0); + this.rightLabel = this.items.at(1); + + var leftLabel = this.model.get("leftLabel") || this.leftLabel.get("value"); + var rightLabel = this.model.get("rightLabel") || this.rightLabel.get("value"); + + var colors = this._generateColorList(); + + var options = _.extend( this.model.toJSON(), { leftLabel: leftLabel, rightLabel: rightLabel, colors: colors, buckets_count: colors.length }); + + this.$el.html(this.template(options)); + } + } + + return this; + + } + +}); + +/* + * Density Legend public interface + * + * */ +cdb.geo.ui.Legend.Density = cdb.geo.ui.DensityLegend.extend({ + + type: "density", + + className: "cartodb-legend density", + + initialize: function() { + + this.items = this.options.items; + + this.model = new cdb.geo.ui.LegendModel({ + type: this.type, + title: this.options.title, + show_title: this.options.title ? true : false, + leftLabel: this.options.left || this.options.leftLabel, + rightLabel: this.options.right || this.options.rightLabel, + colors: this.options.colors, + buckets_count: this.options.colors ? this.options.colors.length : 0, + items: this.options.items + }); + + this._bindModel(); + + }, + + _bindModel: function() { + + this.model.bind("change:colors change:template change:title change:show_title change:colors change:leftLabel change:rightLabel", this.render, this); + + }, + + _generateColorList: function() { + + return _.map(this.model.get("colors"), function(color) { + return '
'; + }).join(""); + + }, + + render: function() { + + var options = _.extend(this.model.toJSON(), { colors: this._generateColorList() }); + + this.$el.html(this.template(options)); + + return this; + + } + +}); + +/* + * IntensityLegend + * + * */ +cdb.geo.ui.IntensityLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "intensity-legend", + + template: _.template('<% if (title && show_title) { %>\n
<%- title %>
<% } %>
  • \t<%- leftLabel %>
  • \t<%- rightLabel %>
'), + + initialize: function() { + + this.items = this.model.items; + + }, + + _bindModel: function() { + + this.model.bind("change:template", this.render, this); + + }, + + setColor: function(color) { + + this.model.set("color", color); + + }, + + setLeftLabel: function(text) { + + this.model.set("leftLabel", text); + + }, + + setRightLabel: function(text) { + + this.model.set("rightLabel", text); + + }, + + _hexToRGB: function(hex) { + + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + + }, + + _rgbToHex: function(r, g, b) { + + function componentToHex(c) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; + } + + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); + }, + + _calculateMultiply: function(color, steps) { + + var colorHex = this._hexToRGB(color); + + if (colorHex) { + + var r = colorHex.r; + var g = colorHex.g; + var b = colorHex.b; + + for (var i = 0; i <= steps; i++) { + r = Math.round(r * colorHex.r/255); + g = Math.round(g * colorHex.g/255); + b = Math.round(b * colorHex.b/255); + } + + return this._rgbToHex(r,g,b); + + } + + return "#ffffff"; + + }, + + _renderGraph: function(baseColor) { + + var s = ""; + + s+= "background: <%= color %>;"; + s+= "background: -moz-linear-gradient(left, <%= color %> 0%, <%= right %> 100%);"; + s+= "background: -webkit-gradient(linear, left top, right top, color-stop(0%,<%= color %>), color-stop(100%,<%= right %>));"; + s+= "background: -webkit-linear-gradient(left, <%= color %> 0%,<%= right %> 100%);"; + s+= "background: -o-linear-gradient(left, <%= color %> 0%,<%= right %> 100%);"; + s+= "background: -ms-linear-gradient(left, <%= color %> 0%,<%= right %> 100%)"; + s+= "background: linear-gradient(to right, <%= color %> 0%,<%= right %> 100%);"; + s+= "filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='<%= color %>', endColorstr='<%= right %>',GradientType=1 );"; + s+= "background-image: -ms-linear-gradient(left, <%= color %> 0%,<%= right %> 100%)"; + + var backgroundStyle = _.template(s); + + var multipliedColor = this._calculateMultiply(baseColor, 4); + + this.$el.find(".graph").attr("style", backgroundStyle({ color: baseColor, right: multipliedColor })); + + }, + + render: function() { + + if (this.model.get("template")) { + + var template = _.template(cdb.core.sanitize.html(this.model.get("template"), this.model.get('sanitizeTemplate'))); + this.$el.html(template(this.model.toJSON())); + + } else { + + if (this.items.length >= 3) { + + this.leftLabel = this.items.at(0); + this.rightLabel = this.items.at(1); + var color = this.model.get("color") || this.items.at(2).get("value"); + + var leftLabel = this.model.get("leftLabel") || this.leftLabel.get("value"); + var rightLabel = this.model.get("rightLabel") || this.rightLabel.get("value"); + + var options = _.extend( this.model.toJSON(), { color: color, leftLabel: leftLabel, rightLabel: rightLabel }); + + this.$el.html(this.template(options)); + + this._renderGraph(color); + } + + } + + return this; + + } + +}); + +/* + * CategoryLegend + * + * */ +cdb.geo.ui.CategoryLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "category-legend", + + template: _.template('<% if (title && show_title) { %>\n
<%- title %>
<% } %>
    '), + + initialize: function() { + + this.items = this.model.items; + + }, + + _bindModel: function() { + + this.model.bind("change:title change:show_title change:template", this.render, this); + + }, + + _renderItems: function() { + + this.items.each(this._renderItem, this); + + }, + + _renderItem: function(item) { + + view = new cdb.geo.ui.LegendItem({ + model: item, + className: (item.get("value") && item.get("value").indexOf("http") >= 0 || item.get("type") && item.get("type") == 'image') ? "bkg" : "", + template: '\t\t
    <%- name || ((name === false) ? "false": "null") %>' + }); + + this.$el.find("ul").append(view.render()); + + }, + + render: function() { + + if (this.model.get("template")) { + + var template = _.template(cdb.core.sanitize.html(this.model.get("template"), this.model.get('sanitizeTemplate'))); + this.$el.html(template(this.model.toJSON())); + + } else { + + this.$el.html(this.template(this.model.toJSON())); + + if (this.items.length > 0) { + this._renderItems(); + } else { + this.$el.html('
    The category legend is empty
    '); + } + } + + return this; + + } + +}); + +/* + * Category Legend public interface + * + * */ +cdb.geo.ui.Legend.Category = cdb.geo.ui.CategoryLegend.extend({ + + className: "cartodb-legend category", + + type: "category", + + initialize: function() { + + this.items = new cdb.geo.ui.LegendItems(this.options.data); + + this.model = new cdb.geo.ui.LegendModel({ + type: this.type, + title: this.options.title, + show_title: this.options.title ? true : false + }); + + this._bindModel(); + + }, + + render: function() { + + this.$el.html(this.template(this.model.toJSON())); + + this._renderItems(); + + return this; + + } + +}); + +/* + * ColorLegend + * + * */ +cdb.geo.ui.ColorLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "color-legend", + + type: "color", + + template: _.template('<% if (title && show_title) { %>\n
    <%- title %>
    <% } %>
      '), + + initialize: function() { + + this.items = this.model.items; + + }, + + _renderItems: function() { + + this.items.each(this._renderItem, this); + + }, + + _renderItem: function(item) { + + view = new cdb.geo.ui.LegendItem({ + model: item, + className: (item.get("value") && item.get("value").indexOf("http") >= 0) ? "bkg" : "", + template: '\t\t
      <%- name || ((name === false) ? "false": "null") %>' + }); + + this.$el.find("ul").append(view.render()); + + }, + + render: function() { + + this.$el.html(this.template(this.model.toJSON())); + + if (this.items.length > 0) { + this._renderItems(); + } else { + this.$el.html('
      The color legend is empty
      '); + } + + return this; + + } + +}); + +/* + * Color Legend public interface + * + * */ +cdb.geo.ui.Legend.Color = cdb.geo.ui.Legend.Category.extend({ }); + +/* + * StackedLegend + * + * */ +cdb.geo.ui.StackedLegend = cdb.core.View.extend({ + + events: { + "dragstart": "_stopPropagation", + "mousedown": "_stopPropagation", + "touchstart": "_stopPropagation", + "MSPointerDown": "_stopPropagation", + "dblclick": "_stopPropagation", + "mousewheel": "_stopPropagation", + "DOMMouseScroll": "_stopPropagation", + "dbclick": "_stopPropagation", + "click": "_stopPropagation" + }, + + className: "cartodb-legend-stack", + + initialize: function() { + _.each(this.options.legends, this._setupBinding, this); + }, + + _stopPropagation: function(ev) { + ev.stopPropagation(); + }, + + getLegendByIndex: function(index) { + if (!this._layerByIndex) { + this._layerByIndex = {}; + var legends = this.options.legends; + for (var i = 0; i < legends.length; ++i) { + var legend = legends[i]; + this._layerByIndex[legend.options.index] = legend; + } + } + return this._layerByIndex[index]; + }, + + _setupBinding: function(legend) { + legend.model.bind("change:type", this._checkVisibility, this); + this.add_related_model(legend.model); + }, + + render: function() { + this._renderItems(); + this._checkVisibility(); + + return this; + }, + + _renderItems: function() { + _.each(this.options.legends, function(item) { + this.$el.append(item.render().$el); + }, this); + }, + + _checkVisibility: function() { + var visible = _.some(this.options.legends, function(legend) { + return legend.model.get("type") && (legend.model.get("type") != "none" || legend.model.get("template")) + }, this); + + if (visible) { + this.show(); + } else { + this.hide(); + } + + _.each(this.options.legends, function(item) { + var legendModel = item.model; + if (legendModel.get("type") === "none" || legendModel.get("visible") === false) { + item.hide(); + } else { + item.show(); + } + }, this); + }, + + show: function() { + this.$el.show(); + }, + + hide: function() { + this.$el.hide(); + }, + + addTo: function(element) { + $(element).html(this.render().$el); + } +}); + + +/* + * Stacked Legend public interface + * + * */ +cdb.geo.ui.Legend.Stacked = cdb.geo.ui.StackedLegend.extend({ + + initialize: function() { + + if (this.options.legends) { + + var legendModels = _.map(this.options.legends, function(legend) { + return legend.model; + }); + + this.legendItems = new cdb.geo.ui.Legends(legendModels); + + this.legendItems.bind("add remove change", this.render, this); + + } else if (this.options.data) { + + var legendModels = _.map(this.options.data, function(legend) { + return new cdb.geo.ui.LegendModel(legend); + }); + + this.legendItems = new cdb.geo.ui.Legends(legendModels); + + this.legendItems.bind("add remove change", this.render, this); + + } + + }, + + _capitalize: function(string) { + if (string && _.isString(string)) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + }, + + render: function() { + + this.$el.empty(); + + this.legends = []; + + if (this.legendItems && this.legendItems.length > 0) { + + this.legendItems.each(this._renderLegend, this); + + } + + return this; + + }, + + _renderLegend: function(model) { + + var type = model.get("type"); + + if (!type) type = "custom"; + + type = this._capitalize(type); + + var view = new cdb.geo.ui.Legend[type](model.attributes); + + this.legends.push(view); + + if (model.get("visible") !== false) this.$el.append(view.render().$el); + + }, + + getLegendAt: function(n) { + + return this.legends[n]; + + }, + + addLegend: function(attributes) { + + var legend = new cdb.geo.ui.LegendModel(attributes); + this.legendItems.push(legend); + + }, + + removeLegendAt: function(n) { + + var legend = this.legendItems.at(n); + this.legendItems.remove(legend); + + } + +}); + +/* + * CustomLegend + * + * */ +cdb.geo.ui.CustomLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "custom-legend", + type: "custom", + + template: _.template('<% if (title && show_title) { %>\n
      <%- title %>
      <% } %>
        '), + + initialize: function() { + this.items = this.model.items; + }, + + setData: function(data) { + + this.items = new cdb.geo.ui.LegendItems(data); + this.model.items = this.items; + this.model.set("items", data); + + }, + + _renderItems: function() { + + this.items.each(this._renderItem, this); + + }, + + _renderItem: function(item) { + + var template = this.options.itemTemplate || '\t\t
        \n\t\t<%- name || "null" %>'; + + view = new cdb.geo.ui.LegendItem({ + model: item, + className: (item.get("value") && item.get("value").indexOf("http") >= 0) ? "bkg" : "", + template: template + }); + + this.$el.find("ul").append(view.render()); + + }, + + render: function() { + + if (this.model.get("template")) { + + var template = _.template(cdb.core.sanitize.html(this.model.get("template"), this.model.get('sanitizeTemplate'))); + this.$el.html(template(this.model.toJSON())); + + } else { + + this.$el.html(this.template(this.model.toJSON())); + + if (this.items.length > 0) { + this._renderItems(); + } else { + this.$el.html('
        The legend is empty
        '); + } + } + + return this; + + } + +}); + +/* + * Custom Legend public interface + * + * */ +cdb.geo.ui.Legend.Custom = cdb.geo.ui.CustomLegend.extend({ + + className: "cartodb-legend custom", + + type: "custom", + + initialize: function() { + + this.items = new cdb.geo.ui.LegendItems(this.options.data || this.options.items); + + this.model = new cdb.geo.ui.LegendModel({ + type: this.type, + title: this.options.title, + show_title: this.options.title ? true : false, + items: this.items.models + }); + + this._bindModel(); + + }, + + _bindModel: function() { + + this.model.bind("change:items change:template change:title change:show_title", this.render, this); + + } + +}); + +/* + * BubbleLegend + * + * */ +cdb.geo.ui.BubbleLegend = cdb.geo.ui.BaseLegend.extend({ + + className: "bubble-legend", + + template: _.template('<% if (title && show_title) { %>\n
        <%- title %>
        <% } %>
        • \t<%- min %>
        • \t\t
        • \t<%- max %>
        '), + + initialize: function() { + + this.items = this.model.items; + + }, + + _bindModel: function() { + + this.model.bind("change:template change:title change:show_title change:color change:min change:max", this.render, this); + + }, + + setColor: function(color) { + this.model.set("color", color); + }, + + setMinValue: function(value) { + this.model.set("min", value); + }, + + setMaxValue: function(value) { + this.model.set("max", value); + }, + + _renderGraph: function(color) { + this.$el.find(".graph").css("background", color); + }, + + render: function() { + + if (this.model.get("template")) { + + var template = _.template(cdb.core.sanitize.html(this.model.get("template"), this.model.get('sanitizeTemplate'))); + this.$el.html(template(this.model.toJSON())); + + this.$el.removeClass("bubble-legend"); + + } else { + + var color = this.model.get("color") || (this.items.length >= 3 ? this.items.at(2).get("value") : ""); + + if (this.items.length >= 3) { + + var min = this.model.get("min") || this.items.at(0).get("value"); + var max = this.model.get("max") || this.items.at(1).get("value"); + + var options = _.extend(this.model.toJSON(), { min: min, max: max }); + + this.$el.html(this.template(options)); + + } + + this._renderGraph(color); + } + + return this; + + } + +}); + + +/* + * Bubble Legend public interface + * + * */ +cdb.geo.ui.Legend.Bubble = cdb.geo.ui.BubbleLegend.extend({ + + className: "cartodb-legend bubble", + + type: "bubble", + + initialize: function() { + + this.model = new cdb.geo.ui.LegendModel({ + type: this.type, + title: this.options.title, + min: this.options.min, + max: this.options.max, + color: this.options.color, + show_title: this.options.title ? true : false + }); + + this.add_related_model(this.model); + + this._bindModel(); + + }, + + render: function() { + + this.$el.html(this.template(this.model.toJSON())); + + this._renderGraph(this.model.get("color")); + + return this; + + } + +}); + +/* + * Choropleth Legend public interface + * + * */ +cdb.geo.ui.Legend.Choropleth = cdb.geo.ui.ChoroplethLegend.extend({ + + type: "choropleth", + + className: "cartodb-legend choropleth", + + initialize: function() { + + this.items = this.options.items; + + this.model = new cdb.geo.ui.LegendModel({ + type: this.type, + title: this.options.title, + show_title: this.options.title ? true : false, + leftLabel: this.options.left || this.options.leftLabel, + rightLabel: this.options.right || this.options.rightLabel, + colors: this.options.colors, + buckets_count: this.options.colors ? this.options.colors.length : 0 + }); + + this.add_related_model(this.model); + this._bindModel(); + + }, + + _bindModel: function() { + + this.model.bind("change:template change:title change:show_title change:colors change:leftLabel change:rightLabel", this.render, this); + + }, + + _generateColorList: function() { + + return _.map(this.model.get("colors"), function(color) { + return '\t\t
        '; + }).join(""); + + }, + + render: function() { + + var options = _.extend(this.model.toJSON(), { colors: this._generateColorList() }); + + this.$el.html(this.template(options)); + + return this; + + } + +}); + + +/* + * Intensity Legend public interface + * + * */ +cdb.geo.ui.Legend.Intensity = cdb.geo.ui.IntensityLegend.extend({ + + className: "cartodb-legend intensity", + type: "intensity", + + initialize: function() { + + this.items = this.options.items; + + this.model = new cdb.geo.ui.LegendModel({ + type: this.type, + title: this.options.title, + show_title: this.options.title ? true : false, + color: this.options.color, + leftLabel: this.options.left || this.options.leftLabel, + rightLabel: this.options.right || this.options.rightLabel + }); + + this.add_related_model(this.model); + this._bindModel(); + + }, + + _bindModel: function() { + + this.model.bind("change:title change:show_title change:color change:leftLabel change:rightLabel", this.render, this); + + }, + + render: function() { + + this.$el.html(this.template(this.model.toJSON())); + + this._renderGraph(this.model.get("color")); + + return this; + + } + +}); + +cdb.geo.ui.SwitcherItemModel = Backbone.Model.extend({ }); + +cdb.geo.ui.SwitcherItems = Backbone.Collection.extend({ + model: cdb.geo.ui.SwitcherItemModel +}); + +cdb.geo.ui.SwitcherItem = cdb.core.View.extend({ + + tagName: "li", + + events: { + + "click a" : "select" + + }, + + initialize: function() { + + _.bindAll(this, "render"); + this.template = cdb.templates.getTemplate('templates/map/switcher/item'); + this.parent = this.options.parent; + this.model.on("change:selected", this.render); + + }, + + select: function(e) { + e.preventDefault(); + this.parent.toggle(this); + var callback = this.model.get("callback"); + + if (callback) { + callback(); + } + + }, + + render: function() { + + if (this.model.get("selected") == true) { + this.$el.addClass("selected"); + } else { + this.$el.removeClass("selected"); + } + + this.$el.html(this.template(this.model.toJSON())); + return this.$el; + + } + +}); + +cdb.geo.ui.Switcher = cdb.core.View.extend({ + + id: "switcher", + + default_options: { + + }, + + initialize: function() { + + this.map = this.model; + + this.add_related_model(this.model); + + _.bindAll(this, "render", "show", "hide", "toggle"); + + _.defaults(this.options, this.default_options); + + if (this.collection) { + this.model.collection = this.collection; + } + + this.template = this.options.template ? this.options.template : cdb.templates.getTemplate('geo/switcher'); + }, + + show: function() { + this.$el.fadeIn(250); + }, + + hide: function() { + this.$el.fadeOut(250); + }, + + toggle: function(clickedItem) { + + if (this.collection) { + this.collection.each(function(item) { + item.set("selected", !item.get("selected")); + }); + } + + }, + + render: function() { + var self = this; + + if (this.model != undefined) { + this.$el.html(this.template(this.model.toJSON())); + } + + if (this.collection) { + + this.collection.each(function(item) { + + var view = new cdb.geo.ui.SwitcherItem({ parent: self, className: item.get("className"), model: item }); + self.$el.find("ul").append(view.render()); + + }); + } + + return this; + } + +}); + +/** Usage: + * + * Add Infowindow model: + * + * var infowindowModel = new cdb.geo.ui.InfowindowModel({ + * template_name: 'infowindow_light', + * latlng: [72, -45], + * offset: [100, 10] + * }); + * + * var infowindow = new cdb.geo.ui.Infowindow({ + * model: infowindowModel, + * mapView: mapView + * }); + * + * Show the infowindow: + * infowindow.showInfowindow(); + * + */ + +cdb.geo.ui.InfowindowModel = Backbone.Model.extend({ + + SYSTEM_COLUMNS: ['the_geom', 'the_geom_webmercator', 'created_at', 'updated_at', 'cartodb_id', 'cartodb_georef_status'], + + defaults: { + template_name: 'infowindow_light', + latlng: [0, 0], + offset: [28, 0], // offset of the tip calculated from the bottom left corner + maxHeight: 180, // max height of the content, not the whole infowindow + autoPan: true, + template: "", + content: "", + visibility: false, + alternative_names: { }, + fields: null // contains the fields displayed in the infowindow + }, + + clearFields: function() { + this.set({fields: []}); + }, + + saveFields: function(where) { + where = where || 'old_fields'; + this.set(where, _.clone(this.get('fields'))); + }, + + fieldCount: function() { + var fields = this.get('fields') + if (!fields) return 0; + return fields.length + }, + + restoreFields: function(whiteList, from) { + from = from || 'old_fields'; + var fields = this.get(from); + if(whiteList) { + fields = fields.filter(function(f) { + return _.contains(whiteList, f.name); + }); + } + if(fields && fields.length) { + this._setFields(fields); + } + this.unset(from); + }, + + _cloneFields: function() { + return _(this.get('fields')).map(function(v) { + return _.clone(v); + }); + }, + + _setFields: function(f) { + f.sort(function(a, b) { return a.position - b.position; }); + this.set({'fields': f}); + }, + + sortFields: function() { + this.get('fields').sort(function(a, b) { return a.position - b.position; }); + }, + + _addField: function(fieldName, at) { + var dfd = $.Deferred(); + if(!this.containsField(fieldName)) { + var fields = this.get('fields'); + if(fields) { + at = at === undefined ? fields.length: at; + fields.push({ name: fieldName, title: true, position: at }); + } else { + at = at === undefined ? 0 : at; + this.set('fields', [{ name: fieldName, title: true, position: at }], { silent: true}); + } + } + dfd.resolve(); + return dfd.promise(); + }, + + addField: function(fieldName, at) { + var self = this; + $.when(this._addField(fieldName, at)).then(function() { + self.sortFields(); + self.trigger('change:fields'); + self.trigger('add:fields'); + }); + return this; + }, + + getFieldProperty: function(fieldName, k) { + if(this.containsField(fieldName)) { + var fields = this.get('fields') || []; + var idx = _.indexOf(_(fields).pluck('name'), fieldName); + return fields[idx][k]; + } + return null; + }, + + setFieldProperty: function(fieldName, k, v) { + if(this.containsField(fieldName)) { + var fields = this._cloneFields() || []; + var idx = _.indexOf(_(fields).pluck('name'), fieldName); + fields[idx][k] = v; + this._setFields(fields); + } + return this; + }, + + getAlternativeName: function(fieldName) { + return this.get("alternative_names") && this.get("alternative_names")[fieldName]; + }, + + setAlternativeName: function(fieldName, alternativeName) { + var alternativeNames = this.get("alternative_names") || []; + + alternativeNames[fieldName] = alternativeName; + this.set({ 'alternative_names': alternativeNames }); + this.trigger('change:alternative_names'); + }, + + getFieldPos: function(fieldName) { + var p = this.getFieldProperty(fieldName, 'position'); + if(p == undefined) { + return Number.MAX_VALUE; + } + return p; + }, + + containsAlternativeName: function(fieldName) { + var names = this.get('alternative_names') || []; + return names[fieldName]; + }, + + containsField: function(fieldName) { + var fields = this.get('fields') || []; + return _.contains(_(fields).pluck('name'), fieldName); + }, + + removeField: function(fieldName) { + if(this.containsField(fieldName)) { + var fields = this._cloneFields() || []; + var idx = _.indexOf(_(fields).pluck('name'), fieldName); + if(idx >= 0) { + fields.splice(idx, 1); + } + this._setFields(fields); + this.trigger('remove:fields') + } + return this; + }, + + // updates content with attributes + updateContent: function(attributes) { + var fields = this.get('fields'); + this.set('content', cdb.geo.ui.InfowindowModel.contentForFields(attributes, fields)); + }, + + closeInfowindow: function(){ + if (this.get('visibility')) { + this.set("visibility", false); + this.trigger('close'); + } + } + +}, { + contentForFields: function(attributes, fields, options) { + options = options || {}; + var render_fields = []; + for(var j = 0; j < fields.length; ++j) { + var field = fields[j]; + var value = attributes[field.name]; + if(options.empty_fields || (value !== undefined && value !== null)) { + render_fields.push({ + title: field.title ? field.name : null, + value: attributes[field.name], + index: j + }); + } + } + + // manage when there is no data to render + if (render_fields.length === 0) { + render_fields.push({ + title: null, + value: 'No data available', + index: 0, + type: 'empty' + }); + } + + return { + fields: render_fields, + data: attributes + }; + } +}); + +cdb.geo.ui.Infowindow = cdb.core.View.extend({ + className: "cartodb-infowindow", + + spin_options: { + lines: 10, length: 0, width: 4, radius: 6, corners: 1, rotate: 0, color: 'rgba(0,0,0,0.5)', + speed: 1, trail: 60, shadow: false, hwaccel: true, className: 'spinner', zIndex: 2e9, + top: 'auto', left: 'auto', position: 'absolute' + }, + + events: { + // Close bindings + "click .close": "_closeInfowindow", + "touchstart .close": "_closeInfowindow", + "MSPointerDown .close": "_closeInfowindow", + // Rest infowindow bindings + "dragstart": "_checkOrigin", + "mousedown": "_checkOrigin", + "touchstart": "_checkOrigin", + "MSPointerDown": "_checkOrigin", + "dblclick": "_stopPropagation", + "DOMMouseScroll": "_stopBubbling", + 'MozMousePixelScroll': "_stopBubbling", + "mousewheel": "_stopBubbling", + "dbclick": "_stopPropagation", + "click": "_stopPropagation" + }, + + initialize: function(){ + var self = this; + + _.bindAll(this, "render", "setLatLng", "_setTemplate", "_updatePosition", + "_update", "toggle", "show", "hide"); + + this.mapView = this.options.mapView; + + // Set template if it is defined in options + if (this.options.template) this.model.set('template', this.options.template); + + // Set template view variable and + // compile it if it is necessary + if (this.model.get('template')) { + this._compileTemplate(); + } else { + this._setTemplate(); + } + + this.model.bind('change:content', this.render, this); + this.model.bind('change:template_name', this._setTemplate, this); + this.model.bind('change:latlng', this._update, this); + this.model.bind('change:visibility', this.toggle, this); + this.model.bind('change:template', this._compileTemplate, this); + this.model.bind('change:sanitizeTemplate', this._compileTemplate, this); + this.model.bind('change:alternative_names', this.render, this); + this.model.bind('change:width', this.render, this); + this.model.bind('change:maxHeight', this.render, this); + + this.mapView.map.bind('change', this._updatePosition, this); + + this.mapView.bind('zoomstart', function(){ + self.hide(true); + }); + + this.mapView.bind('zoomend', function() { + self.show(true); + }); + + this.add_related_model(this.mapView.map); + + // Hide the element + this.$el.hide(); + }, + + + /** + * Render infowindow content + */ + render: function() { + + if(this.template) { + + // If there is content, destroy the jscrollpane first, then remove the content. + var $jscrollpane = this.$(".cartodb-popup-content"); + if ($jscrollpane.length > 0 && $jscrollpane.data() != null) { + $jscrollpane.data().jsp && $jscrollpane.data().jsp.destroy(); + } + + // Clone fields and template name + var fields = _.map(this.model.attributes.content.fields, function(field){ + return _.clone(field); + }); + var data = this.model.get('content') ? this.model.get('content').data : {}; + + // If a custom template is not applied, let's sanitized + // fields for the template rendering + if (this.model.get('template_name')) { + var template_name = _.clone(this.model.attributes.template_name); + + // Sanitized them + fields = this._fieldsToString(fields, template_name); + } + + // Join plan fields values with content to work with + // custom infowindows and CartoDB infowindows. + var values = {}; + _.each(this.model.get('content').fields, function(pair) { + values[pair.title] = pair.value; + }) + + var obj = _.extend({ + content: { + fields: fields, + data: data + } + },values); + + this.$el.html( + cdb.core.sanitize.html(this.template(obj), this.model.get('sanitizeTemplate')) + ); + + // Set width and max-height from the model only + // If there is no width set, we don't force our infowindow + if (this.model.get('width')) { + this.$('.cartodb-popup').css('width', this.model.get('width') + 'px'); + } + this.$('.cartodb-popup .cartodb-popup-content').css('max-height', this.model.get('maxHeight') + 'px'); + + // Hello jscrollpane hacks! + // It needs some time to initialize, if not it doesn't render properly the fields + // Check the height of the content + the header if exists + var self = this; + setTimeout(function() { + var actual_height = self.$(".cartodb-popup-content").outerHeight(); + if (self.model.get('maxHeight') <= actual_height) + self.$(".cartodb-popup-content").jScrollPane({ + verticalDragMinHeight: 20, + autoReinitialise: true + }); + }, 1); + + // If the infowindow is loading, show spin + this._checkLoading(); + + // If the template is 'cover-enabled', load the cover + this._loadCover(); + + if(!this.isLoadingData()) { + this.model.trigger('domready', this, this.$el); + this.trigger('domready', this, this.$el); + } + } + + return this; + }, + + _getModelTemplate: function() { + return this.model.get("template_name") + }, + + /** + * Change template of the infowindow + */ + _setTemplate: function() { + if (this.model.get('template_name')) { + this.template = cdb.templates.getTemplate(this._getModelTemplate()); + this.render(); + } + }, + + /** + * Compile template of the infowindow + */ + _compileTemplate: function() { + var template = this.model.get('template') ? + this.model.get('template') : + cdb.templates.getTemplate(this._getModelTemplate()); + + if(typeof(template) !== 'function') { + this.template = new cdb.core.Template({ + template: template, + type: this.model.get('template_type') || 'mustache' + }).asFunction() + } else { + this.template = template + } + + this.render(); + }, + + /** + * Check event origin + */ + _checkOrigin: function(ev) { + // If the mouse down come from jspVerticalBar + // dont stop the propagation, but if the event + // is a touchstart, stop the propagation + var come_from_scroll = (($(ev.target).closest(".jspVerticalBar").length > 0) && (ev.type != "touchstart")); + + if (!come_from_scroll) { + ev.stopPropagation(); + } + }, + + /** + * Convert values to string unless value is NULL + */ + _fieldsToString: function(fields, template_name) { + var fields_sanitized = []; + if (fields && fields.length > 0) { + var self = this; + fields_sanitized = _.map(fields, function(field,i) { + // Return whole attribute sanitized + return self._sanitizeField(field, template_name, field.index || i); + }); + } + return fields_sanitized; + }, + + /** + * Sanitize fields, what does it mean? + * - If value is null, transform to string + * - If value is an url, add it as an attribute + * - Cut off title if it is very long (in header or image templates). + * - If the value is a valid url, let's make it a link. + * - More to come... + */ + _sanitizeField: function(attr, template_name, pos) { + // Check null or undefined :| and set both to empty == '' + if (attr.value == null || attr.value == undefined) { + attr.value = ''; + } + + //Get the alternative title + var alternative_name = this.model.getAlternativeName(attr.title); + + if (attr.title && alternative_name) { + // Alternative title + attr.title = alternative_name; + } else if (attr.title) { + // Remove '_' character from titles + attr.title = attr.title.replace(/_/g,' '); + } + + // Cast all values to string due to problems with Mustache 0 number rendering + var new_value = attr.value.toString(); + + // If it is index 0, not any field type, header template type and length bigger than 30... cut off the text! + if (!attr.type && pos==0 && attr.value.length > 35 && template_name && template_name.search('_header_') != -1) { + new_value = attr.value.substr(0,32) + "..."; + } + + // If it is index 1, not any field type, header image template type and length bigger than 30... cut off the text! + if (!attr.type && pos==1 && attr.value.length > 35 && template_name && template_name.search('_header_with_image') != -1) { + new_value = attr.value.substr(0,32) + "..."; + } + + // Is it the value a link? + if (this._isValidURL(attr.value)) { + new_value = "" + new_value + "" + } + + // If it is index 0, not any field type, header image template type... don't cut off the text or add any link!! + if (pos==0 && template_name.search('_header_with_image') != -1) { + new_value = attr.value; + } + + // Save new sanitized value + attr.value = new_value; + + return attr; + }, + + isLoadingData: function() { + var content = this.model.get("content"); + return content.fields && content.fields.length == 1 && content.fields[0].type === "loading"; + }, + + /** + * Check if infowindow is loading the row content + */ + _checkLoading: function() { + if (this.isLoadingData()) { + this._startSpinner(); + } else { + this._stopSpinner(); + } + }, + + /** + * Stop loading spinner + */ + _stopSpinner: function() { + if (this.spinner) + this.spinner.stop() + }, + + /** + * Start loading spinner + */ + _startSpinner: function($el) { + this._stopSpinner(); + + var $el = this.$el.find('.loading'); + + if ($el) { + // Check if it is dark or other to change color + var template_dark = this.model.get('template_name').search('dark') != -1; + + if (template_dark) { + this.spin_options.color = '#FFF'; + } else { + this.spin_options.color = 'rgba(0,0,0,0.5)'; + } + + this.spinner = new Spinner(this.spin_options).spin(); + $el.append(this.spinner.el); + } + }, + + /** + * Stop loading spinner + */ + _containsCover: function() { + return this.$el.find(".cartodb-popup.header").attr("data-cover") ? true : false; + }, + + + /** + * Get cover URL + */ + _getCoverURL: function() { + var content = this.model.get("content"); + + if (content && content.fields && content.fields.length > 0) { + return (content.fields[0].value || '').toString(); + } + + return false; + }, + + /** + * Attempts to load the cover URL and show it + */ + _loadCover: function() { + + if (!this._containsCover()) return; + + var self = this; + var $cover = this.$(".cover"); + var $img = $cover.find("img"); + var $shadow = this.$(".shadow"); + var url = this._getCoverURL(); + + if (!this._isValidURL(url)) { + $img.hide(); + $shadow.hide(); + cdb.log.info("Header image url not valid"); + return; + } + + // configure spinner + var target = document.getElementById('spinner'); + var opts = { lines: 9, length: 4, width: 2, radius: 4, corners: 1, rotate: 0, color: '#ccc', speed: 1, trail: 60, shadow: true, hwaccel: false, zIndex: 2e9 }; + var spinner = new Spinner(opts).spin(target); + + // create the image + + $img.hide(function() { + this.remove(); + }); + + $img = $("").attr("src", url); + $cover.append($img); + + $img.load(function(){ + spinner.stop(); + + var w = $img.width(); + var h = $img.height(); + var coverWidth = $cover.width(); + var coverHeight = $cover.height(); + + var ratio = h / w; + var coverRatio = coverHeight / coverWidth; + + // Resize rules + if ( w > coverWidth && h > coverHeight) { // bigger image + if ( ratio < coverRatio ) $img.css({ height: coverHeight }); + else { + var calculatedHeight = h / (w / coverWidth); + $img.css({ width: coverWidth, top: "50%", position: "absolute", "margin-top": -1*parseInt(calculatedHeight, 10)/2 }); + } + } else { + var calculatedHeight = h / (w / coverWidth); + $img.css({ width: coverWidth, top: "50%", position: "absolute", "margin-top": -1*parseInt(calculatedHeight, 10)/2 }); + } + + $img.fadeIn(300); + }) + .error(function(){ + spinner.stop(); + }); + }, + + /** + * Return true if the provided URL is valid + */ + _isValidURL: function(url) { + if (url) { + var urlPattern = /^(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-|]*[\w@?^=%&\/~+#-])?$/ + return String(url).match(urlPattern) != null ? true : false; + } + + return false; + }, + + /** + * Toggle infowindow visibility + */ + toggle: function() { + this.model.get("visibility") ? this.show() : this.hide(); + }, + + /** + * Stop event bubbling + */ + _stopBubbling: function (e) { + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * Stop event propagation + */ + _stopPropagation: function(ev) { + ev.stopPropagation(); + }, + + /** + * Set loading state adding its content + */ + setLoading: function() { + this.model.set({ + content: { + fields: [{ + title: null, + alternative_name: null, + value: 'Loading content...', + index: null, + type: "loading" + }], + data: {} + } + }) + return this; + }, + + /** + * Set loading state adding its content + */ + setError: function() { + this.model.set({ + content: { + fields: [{ + title: null, + alternative_name: null, + value: 'There has been an error...', + index: null, + type: 'error' + }], + data: {} + } + }) + return this; + }, + + /** + * Set the correct position for the popup + */ + setLatLng: function (latlng) { + this.model.set("latlng", latlng); + return this; + }, + + /** + * Close infowindow + */ + _closeInfowindow: function(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + if (this.model.get("visibility")) { + this.model.set("visibility", false); + this.trigger('close'); + } + }, + + /** + * Set visibility infowindow + */ + showInfowindow: function() { + this.model.set("visibility", true); + }, + + /** + * Show infowindow (update, pan, etc) + */ + show: function (no_pan) { + var self = this; + + if (this.model.get("visibility")) { + self.$el.css({ left: -5000 }); + self._update(no_pan); + } + }, + + /** + * Get infowindow visibility + */ + isHidden: function () { + return !this.model.get("visibility"); + }, + + /** + * Set infowindow to hidden + */ + hide: function (force) { + if (force || !this.model.get("visibility")) this._animateOut(); + }, + + /** + * Update infowindow + */ + _update: function (no_pan) { + + if(!this.isHidden()) { + var delay = 0; + + if (!no_pan) { + var delay = this.adjustPan(); + } + + this._updatePosition(); + this._animateIn(delay); + } + }, + + /** + * Animate infowindow to show up + */ + _animateIn: function(delay) { + if (!cdb.core.util.ie || (cdb.core.util.browser.ie && cdb.core.util.browser.ie.version > 8)) { + this.$el.css({ + 'marginBottom':'-10px', + 'display':'block', + opacity:0 + }); + + this.$el + .delay(delay) + .animate({ + opacity: 1, + marginBottom: 0 + },300); + } else { + this.$el.show(); + } + }, + + /** + * Animate infowindow to disappear + */ + _animateOut: function() { + if (!cdb.core.util.ie || (cdb.core.util.browser.ie && cdb.core.util.browser.ie.version > 8)) { + var self = this; + this.$el.animate({ + marginBottom: "-10px", + opacity: "0", + display: "block" + }, 180, function() { + self.$el.css({display: "none"}); + }); + } else { + this.$el.hide(); + } + }, + + /** + * Update the position (private) + */ + _updatePosition: function () { + if(this.isHidden()) return; + + var + offset = this.model.get("offset") + pos = this.mapView.latLonToPixel(this.model.get("latlng")), + x = this.$el.position().left, + y = this.$el.position().top, + containerHeight = this.$el.outerHeight(true), + containerWidth = this.$el.width(), + left = pos.x - offset[0], + size = this.mapView.getSize(), + bottom = -1*(pos.y - offset[1] - size.y); + + this.$el.css({ bottom: bottom, left: left }); + }, + + /** + * Adjust pan to show correctly the infowindow + */ + adjustPan: function (callback) { + var offset = this.model.get("offset"); + + if (!this.model.get("autoPan") || this.isHidden()) { return; } + + var + x = this.$el.position().left, + y = this.$el.position().top, + containerHeight = this.$el.outerHeight(true) + 15, // Adding some more space + containerWidth = this.$el.width(), + pos = this.mapView.latLonToPixel(this.model.get("latlng")), + adjustOffset = {x: 0, y: 0}; + size = this.mapView.getSize() + wait_callback = 0; + + if (pos.x - offset[0] < 0) { + adjustOffset.x = pos.x - offset[0] - 10; + } + + if (pos.x - offset[0] + containerWidth > size.x) { + adjustOffset.x = pos.x + containerWidth - size.x - offset[0] + 10; + } + + if (pos.y - containerHeight < 0) { + adjustOffset.y = pos.y - containerHeight - 10; + } + + if (pos.y - containerHeight > size.y) { + adjustOffset.y = pos.y + containerHeight - size.y; + } + + if (adjustOffset.x || adjustOffset.y) { + this.mapView.panBy(adjustOffset); + wait_callback = 300; + } + + return wait_callback; + } + +}); + + +cdb.geo.ui.Header = cdb.core.View.extend({ + + className: 'cartodb-header', + + initialize: function() { + var extra = this.model.get("extra"); + + this.model.set({ + title: extra.title, + description: extra.description, + show_title: extra.show_title, + show_description: extra.show_description + }, { silent: true }); + }, + + show: function() { + //var display = this.model.get("display"); + var hasTitle = this.model.get("title") && this.model.get("show_title"); + var hasDescription = this.model.get("description") && this.model.get("show_description"); + + if (hasTitle || hasDescription) { + this.$el.show(); + if (hasTitle) this.$el.find(".content div.title").show(); + if (hasDescription) this.$el.find(".content div.description").show(); + } + }, + + // Add target attribute to all links + _setLinksTarget: function(str) { + if (!str) return str; + var reg = new RegExp(/<(a)([^>]+)>/g); + return str.replace(reg, "<$1 target=\"_blank\"$2>"); + }, + + render: function() { + var data = _.clone(this.model.attributes); + data.title = cdb.core.sanitize.html(data.title); + data.description = this._setLinksTarget(cdb.core.sanitize.html(data.description)); + this.$el.html(this.options.template(data)); + + if (this.options.slides) { + this.slides_controller = new cdb.geo.ui.SlidesController({ + transitions: this.options.transitions, + slides: this.options.slides + }); + + this.$el.append(this.slides_controller.render().$el); + } + + if (this.model.get("show_title") || this.model.get("show_description")) { + this.show(); + } else { + this.hide(); + } + + return this; + + } + +}); + +/** + * UI component to place the map in the + * location found by the geocoder. + * + */ + +cdb.geo.ui.Search = cdb.core.View.extend({ + + className: 'cartodb-searchbox', + + _ZOOM_BY_CATEGORY: { + 'building': 18, + 'postal-area': 15, + 'default': 12 + }, + + events: { + "click input[type='text']": '_onFocus', + "submit form": '_onSubmit', + "click": '_stopPropagation', + "dblclick": '_stopPropagation', + "mousedown": '_stopPropagation' + }, + + options: { + searchPin: true, + infowindowTemplate: '
        '+ + '
        '+ + 'x'+ + '
        '+ + '

        {{ address }}

        '+ + '
        '+ + '
        '+ + '
        ', + infowindowWidth: 186, + infowindowOffset: [93, 90], + iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAfCAYAAADXwvzvAAACuklEQVR4Ae3PQ+AsNxzA8e8vo/Xus237vVN9qW3b7qW2bdu2caxt29bu/meSmaTpqW63Pfc7wemTZPh9K/Xv3zhzxIgVrho0aMsLGo2N9o+iuYDwV02E5NJpM7d5fMGC515dMP/7l6dNMc+OGJY9Uq99cVMc33I4LOJXCQBQuXPBglNnDRm0Xa1RAWewP3yL/vJLul99Q/pNm0/b+qsnbLHngXAVgAI4b9KkXWc1m9vV58ykst56lKdMptyokdTKRJUIV1MMTGTgbOTknWABgFo2SSbOjuN9wlgIBrSIJ0yiVG9QUgGxUigRRAlpCQYrBs+A/QClliuXV6ppPVibDPPqi5irL8G+/QY2S3FZhityrLNYBWkAI2G5WTA2nGTthKDTJfP/FH1sCb76nNBa7I8/knba6Eyj8wJjLbk4qlCdAFNClWXKiiL72kGRUkSRhwUuTUm7XTqZ3z3KnMM7QhAFUfiKMZ9OQci+ydFFH32BIsDh8hxjDF2T0y0KtHHUczCg34P3wgesfWhZozstW1R/cJpuohA8dI7cWrSfxqM4gwEOnoJnn4HXBVDHwHnriNr2W3G0I8FEkKufMbjcIw1DC+iCuRw2OBduEYAKDD8drlkGlk6BHwAtIEDioD/QBnsnnHAI7A9YAAAGenwEnPuAd8+DewHcS+CeB3szvL0b7ADE/FWzYf5BCxa9dMvqa7oLll7WbTlsxKkDYRi9dPqhRz743L0PuKtOPMXtutHmm/InKf5Y6Co15Upl8qSCqVajXiEeUTRb6GqNIojoGaLEDwEA6B0KIKL8lH8JBeS/3AgK73qAPfc/tCLiAACUCmyvsJHnphwEAYFStNs/NoHgn2ATWPmlF54b/9GHH/Khn88/+9SywJx/+q0SsKTZbB45d/6CO0aNHnutv3kbYDQg9JAAIRDwF/0EjlkjUi3fkAMAAAAASUVORK5CYII=', + iconAnchor: [7, 31] + }, + + initialize: function() { + this.mapView = this.options.mapView; + this.template = this.options.template; + }, + + render: function() { + this.$el.html(this.template(this.options)); + return this; + }, + + _stopPropagation: function(ev) { + if (ev) { + ev.stopPropagation(); + } + }, + + _onFocus: function(ev) { + if (ev) { + ev.preventDefault(); + $(ev.target).focus(); + } + }, + + _showLoader: function() { + this.$('span.loader').show(); + }, + + _hideLoader: function() { + this.$('span.loader').hide(); + }, + + _onSubmit: function(ev) { + ev.preventDefault(); + var self = this; + var address = this.$('input.text').val(); + + if (!address) { + return; + } + + // Show geocoder loader + this._showLoader(); + // Remove previous pin + this._destroySearchPin(); + cdb.geo.geocoder.NOKIA.geocode(address, function(places) { + self._onResult(places); + // Hide loader + self._hideLoader(); + }); + }, + + _onResult: function(places) { + var position = ''; + var address = this.$('input.text').val(); + + if (places && places.length>0) { + var location = places[0]; + var validBBox = this._isBBoxValid(location); + + // Get BBox if possible and set bounds + if (validBBox) { + var s = parseFloat(location.boundingbox.south); + var w = parseFloat(location.boundingbox.west); + var n = parseFloat(location.boundingbox.north); + var e = parseFloat(location.boundingbox.east); + + var centerLon = (w + e)/2; + var centerLat = (s + n)/2; + position = [centerLat, centerLon]; + this.model.setBounds([ [ s, w ], [ n, e ] ]); + } + + // If location is defined, + // let's store it + if (location.lat && location.lon) { + position = [location.lat, location.lon]; + } + + // In the case that BBox is not valid, let's + // center the map using the position + if (!validBBox) { + this.model.setCenter(position); + this.model.setZoom(this._getZoomByCategory(location.type)); + } + + if (this.options.searchPin) { + this._createSearchPin(position, address); + } + } + }, + + // Getting zoom for each type of location + _getZoomByCategory: function(type) { + if (type && this._ZOOM_BY_CATEGORY[type]) { + return this._ZOOM_BY_CATEGORY[type]; + } + return this._ZOOM_BY_CATEGORY['default']; + }, + + _isBBoxValid: function(location) { + if(!location.boundingbox || location.boundingbox.south == location.boundingbox.north || + location.boundingbox.east == location.boundingbox.west) { + return false; + } + return true; + }, + + _createSearchPin: function(position, address) { + this._destroySearchPin(); + this._createPin(position, address); + this._createInfowindow(position, address); + this._bindEvents(); + }, + + _destroySearchPin: function() { + this._unbindEvents(); + this._destroyPin(); + this._destroyInfowindow() + }, + + _createInfowindow: function(position, address) { + var infowindowModel = new cdb.geo.ui.InfowindowModel({ + template: this.options.infowindowTemplate, + latlng: position, + width: this.options.infowindowWidth, + offset: this.options.infowindowOffset, + content: { + fields: [{ + title: 'address', + value: address + }] + } + }); + + this._searchInfowindow = new cdb.geo.ui.Infowindow({ + model: infowindowModel, + mapView: this.mapView + }); + + this.mapView.$el.append(this._searchInfowindow.el); + infowindowModel.set('visibility', true); + }, + + _destroyInfowindow: function() { + if (this._searchInfowindow) { + // Hide it and then destroy it (when animation ends) + this._searchInfowindow.hide(true); + var infowindow = this._searchInfowindow; + setTimeout(function() { + infowindow.clean(); + }, 1000); + } + }, + + _createPin: function(position, address) { + this._searchPin = this.mapView._addGeomToMap( + new cdb.geo.Geometry({ + geojson: { type: "Point", "coordinates": [ position[1], position[0] ] }, + iconUrl: this.options.iconUrl, + iconAnchor: this.options.iconAnchor + }) + ); + }, + + _toggleSearchInfowindow: function() { + var infowindowVisibility = this._searchInfowindow.model.get('visibility'); + this._searchInfowindow.model.set('visibility', !infowindowVisibility); + }, + + _destroyPin: function() { + if (this._searchPin) { + this.mapView._removeGeomFromMap(this._searchPin); + delete this._searchPin; + } + }, + + _bindEvents: function() { + this._searchPin && this._searchPin.bind('click', this._toggleSearchInfowindow, this); + this.mapView.bind('click', this._destroySearchPin, this); + }, + + _unbindEvents: function() { + this._searchPin && this._searchPin.unbind('click', this._toggleSearchInfowindow, this); + this.mapView.unbind('click', this._destroySearchPin, this); + }, + + clean: function() { + this._unbindEvents(); + this._destroySearchPin(); + this.elder('clean'); + } + +}); + + +/** + * Layer selector: it allows to select the layers that will be shown in the map + * - It needs the mapview, the element template and the dropdown template + * + * var layer_selector = new cdb.geo.ui.LayerSelector({ + * mapView: mapView, + * template: element_template, + * dropdown_template: dropdown_template + * }); + */ + +cdb.geo.ui.LayerSelector = cdb.core.View.extend({ + + className: 'cartodb-layer-selector-box', + + events: { + "click": '_openDropdown', + "dblclick": 'killEvent', + "mousedown": 'killEvent' + }, + + initialize: function() { + this.map = this.options.mapView.map; + + this.mapView = this.options.mapView; + this.mapView.bind('click zoomstart drag', function() { + this.dropdown && this.dropdown.hide() + }, this); + this.add_related_model(this.mapView); + + this.layers = []; + }, + + render: function() { + + this.$el.html(this.options.template(this.options)); + + this.dropdown = new cdb.ui.common.Dropdown({ + className:"cartodb-dropdown border", + template: this.options.dropdown_template, + target: this.$el.find("a"), + speedIn: 300, + speedOut: 200, + position: "position", + tick: "right", + vertical_position: "down", + horizontal_position: "right", + vertical_offset: 7, + horizontal_offset: 13 + }); + + if (cdb.god) cdb.god.bind("closeDialogs", this.dropdown.hide, this.dropdown); + + this.$el.append(this.dropdown.render().el); + + this._getLayers(); + this._setCount(); + + return this; + }, + + _getLayers: function() { + var self = this; + this.layers = []; + + _.each(this.map.layers.models, function(layer) { + + if (layer.get("type") == 'layergroup' || layer.get('type') === 'namedmap') { + + layer.layers.each(function(layerModel, index){ + var layerName = layerModel.get('layer_name'); + if(self.options.layer_names) { + layerName = self.options.layer_names[index]; + } + + var m = new cdb.core.Model({ + order: index, + visible: layerModel.get('visible') || true, + layer_name: layerName + }); + + m.bind('change:visible', function(model) { + this.trigger("change:visible", model.get('visible'), model.get('order'), model); + layerModel.set('visible', model.get('visible')); + }, self); + + layerModel.bind('change:visible', function() { + m.set('visible', layerModel.get('visible')); + }); + + var layerView = self._createLayerView(m); + layerView.bind('switchChanged', self._setCount, self); + self.layers.push(layerView); + }) + } else if (layer.get("type") === "CartoDB" || layer.get('type') === 'torque') { + var layerView = self._createLayerView(layer); + layerView.bind('switchChanged', self._setCount, self); + self.layers.push(layerView); + layerView.model.bind('change:visible', function(model) { + this.trigger("change:visible", model.get('visible'), model.get('order'), model); + }, self); + } + + }); + }, + + _createLayerView: function(model) { + var layerView = new cdb.geo.ui.LayerView({ + model: model + }); + this.$("ul").append(layerView.render().el); + this.addView(layerView); + return layerView; + }, + + _setCount: function() { + var count = 0; + for (var i = 0, l = this.layers.length; i < l; ++i) { + var lyr = this.layers[i]; + + if (lyr.model.get('visible')) { + count++; + } + } + + this.$('.count').text(count); + this.trigger("switchChanged", this); + }, + + _openDropdown: function() { + this.dropdown.open(); + } + +}); + + +/** + * View for each CartoDB layer + * - It needs a model to make it work. + * + * var layerView = new cdb.geo.ui.LayerView({ + * model: layer_model, + * layer_definition: layer_definition + * }); + * + */ +cdb.geo.ui.LayerView = cdb.core.View.extend({ + + tagName: "li", + + defaults: { + template: '\ + <%- layer_name %>\ + switch">\ + ' + }, + + events: { + "click": '_onSwitchClick' + }, + + initialize: function() { + + if (!this.model.has('visible')) this.model.set('visible', false); + + this.model.bind("change:visible", this._onSwitchSelected, this); + + this.add_related_model(this.model); + + this._onSwitchSelected(); + + // Template + this.template = this.options.template ? cdb.templates.getTemplate(this.options.template) : _.template(this.defaults.template); + }, + + render: function() { + var attrs = _.clone(this.model.attributes); + attrs.layer_name = attrs.layer_name || attrs.table_name; + this.$el.append(this.template(attrs)); + return this; + }, + + /* + * Throw an event when the user clicks in the switch button + */ + _onSwitchSelected: function() { + var enabled = this.model.get('visible'); + + // Change switch + this.$el.find(".switch") + .removeClass(enabled ? 'disabled' : 'enabled') + .addClass(enabled ? 'enabled' : 'disabled'); + + // Send trigger + this.trigger('switchChanged'); + + }, + + _onSwitchClick: function(e){ + this.killEvent(e); + + // Set model + this.model.set("visible", !this.model.get("visible")); + } + +}); + +cdb.geo.ui.SlidesControllerItem = cdb.core.View.extend({ + + tagName: "li", + + events: { + "click a": "_onClick", + }, + + template: cdb.core.Template.compile(''), + + initialize: function() { + + this.model = new cdb.core.Model(this.options); + this.model.bind("change:active", this._onChangeActive, this); + + }, + + _onChangeActive: function(e) { + + if (this.model.get("active")) { + this.$el.find("a").addClass("active"); + } else { + this.$el.find("a").removeClass("active"); + } + + }, + + _onClick: function(e) { + if (e) this.killEvent(e); + this.trigger("onClick", this) + }, + + render: function() { + + var options = _.extend({ transition_trigger: "click" }, this.options.transition_options); + + this.$el.html(this.template(options)); + + this._onChangeActive(); + + return this; + } + +}); + +cdb.geo.ui.SlidesController = cdb.core.View.extend({ + + defaults: { + show_counter: false + }, + + events: { + 'click a.next': "_next", + 'click a.prev': "_prev" + }, + + tagName: "div", + + className: "cartodb-slides-controller", + + template: cdb.core.Template.compile("
        <% if (show_counter) {%>
        <% } else { %>
          <% } %>
          "), + + initialize: function() { + this.slidesCount = this.options.transitions.length; + this.visualization = this.options.visualization; + this.slides = this.visualization.slides; + }, + + _prev: function(e) { + if (e) this.killEvent(e); + this.visualization.sequence.prev(); + }, + + _next: function(e) { + if (e) this.killEvent(e); + this.visualization.sequence.next(); + }, + + _renderDots: function() { + + var currentActiveSlide = this.slides.state(); + + for (var i = 0; i < this.options.transitions.length; i++) { + var item = new cdb.geo.ui.SlidesControllerItem({ num: i, transition_options: this.options.transitions[i], active: i == currentActiveSlide }); + item.bind("onClick", this._onSlideClick, this); + this.$el.find("ul").append(item.render().$el); + } + + }, + + _renderCounter: function() { + + var currentActiveSlide = this.slides.state(); + var currentTransition = this.options.transitions[currentActiveSlide]; + + var $counter = this.$el.find(".counter"); + + if (currentTransition && currentTransition.transition_trigger === "time") { + $counter.addClass("loading"); + } else { + $counter.removeClass("loading"); + } + + $counter.html((currentActiveSlide + 1) + "/" + this.options.transitions.length) + }, + + _onSlideClick: function(slide) { + this.visualization.sequence.current(slide.options.num); + }, + + render: function() { + + var options = _.extend(this.defaults, this.options); + + this.$el.html(this.template(options)); + + if (this.slides && this.options.transitions) { + + if (options.show_counter) { + this._renderCounter(); // we render: 1/N + } else { + this._renderDots(); // we render a list of dots + } + + } + + return this; + } + +}); + +cdb.geo.ui.MobileLayer = cdb.core.View.extend({ + + events: { + 'click h3': "_toggle", + "dblclick": "_stopPropagation" + }, + + tagName: "li", + + className: "cartodb-mobile-layer has-toggle", + + template: cdb.core.Template.compile("<% if (show_title) { %>

          <%- layer_name %><% } %>

          "), + + /** + * Stop event propagation + */ + _stopPropagation: function(ev) { + ev.stopPropagation(); + }, + + initialize: function() { + + _.defaults(this.options, this.default_options); + + this.model.bind("change:visible", this._onChangeVisible, this); + + }, + + _onChangeVisible: function() { + + this.$el.find(".legend")[ this.model.get("visible") ? "fadeIn":"fadeOut"](150); + this.$el[ this.model.get("visible") ? "removeClass":"addClass"]("hidden"); + + this.trigger("change_visibility", this); + + }, + + _toggle: function(e) { + + e.preventDefault(); + e.stopPropagation(); + + if (this.options.hide_toggle) return; + + this.model.set("visible", !this.model.get("visible")) + + }, + + _renderLegend: function() { + + if (!this.options.show_legends) return; + + if (this.model.get("legend") && (this.model.get("legend").type == "none" || !this.model.get("legend").type)) return; + if (this.model.get("legend") && this.model.get("legend").items && this.model.get("legend").items.length == 0) return; + + this.$el.addClass("has-legend"); + + var legend = new cdb.geo.ui.Legend(this.model.get("legend")); + + legend.undelegateEvents(); + + this.$el.append(legend.render().$el); + + }, + + _truncate: function(input, length) { + return input.substr(0, length-1) + (input.length > length ? '…' : ''); + }, + + render: function() { + + var layer_name = this.model.get("layer_name"); + + layer_name = layer_name ? this._truncate(layer_name, 23) : "untitled"; + + var attributes = _.extend( + this.model.attributes, + { + layer_name: this.options.show_title ? layer_name : "", + toggle_class: this.options.hide_toggle ? " hide" : "" + } + ); + + this.$el.html(this.template(_.extend(attributes, { show_title: this.options.show_title } ))); + + + if (this.options.hide_toggle) this.$el.removeClass("has-toggle"); + if (!this.model.get("visible")) this.$el.addClass("hidden"); + if (this.model.get("legend")) this._renderLegend(); + + this._onChangeVisible(); + + return this; + } + +}); + +cdb.geo.ui.Mobile = cdb.core.View.extend({ + + className: "cartodb-mobile", + + events: { + "click .cartodb-attribution-button": "_onAttributionClick", + "click .toggle": "_toggle", + "click .fullscreen": "_toggleFullScreen", + "click .backdrop": "_onBackdropClick", + "dblclick .aside": "_stopPropagation", + "dragstart .aside": "_checkOrigin", + "mousedown .aside": "_checkOrigin", + "touchstart .aside": "_checkOrigin", + "MSPointerDown .aside": "_checkOrigin", + }, + + initialize: function() { + + _.bindAll(this, "_toggle", "_reInitScrollpane"); + + _.defaults(this.options, this.default_options); + + this.hasLayerSelector = false; + this.layersLoading = 0; + + this.slides_data = this.options.slides_data; + this.visualization = this.options.visualization; + + if (this.visualization) { + this.slides = this.visualization.slides; + } + + this.mobileEnabled = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + this.visibility_options = this.options.visibility_options || {}; + + this.mapView = this.options.mapView; + this.map = this.mapView.map; + + this.template = this.options.template ? this.options.template : cdb.templates.getTemplate('geo/zoom'); + + this._selectOverlays(); + + this._setupModel(); + + window.addEventListener('orientationchange', _.bind(this.doOnOrientationChange, this)); + + this._addWheelEvent(); + + }, + + loadingTiles: function() { + if (this.loader) { + this.loader.show() + } + + if (this.layersLoading === 0) { + this.trigger('loading'); + } + this.layersLoading++; + }, + + loadTiles: function() { + if (this.loader) { + this.loader.hide(); + } + this.layersLoading--; + // check less than 0 because loading event sometimes is + // thrown before visualization creation + if(this.layersLoading <= 0) { + this.layersLoading = 0; + this.trigger('load'); + } + }, + + _selectOverlays: function() { + + if (this.slides && this.slides_data) { // if there are slides… + + var state = this.slides.state(); + + if (state == 0) this.overlays = this.options.overlays; // first slide == master vis + else { + this.overlays = this.slides_data[state - 1].overlays; + } + } else { // otherwise we load the regular overlays + this.overlays = this.options.overlays; + } + + }, + + _addWheelEvent: function() { + + var self = this; + var mapView = this.options.mapView; + + $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange', function() { + + if ( !document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement && !document.msFullscreenElement) { + mapView.options.map.set("scrollwheel", false); + } + + mapView.invalidateSize(); + + }); + + }, + + _setupModel: function() { + + this.model = new Backbone.Model({ + open: false, + layer_count: 0 + }); + + this.model.on("change:open", this._onChangeOpen, this); + this.model.on("change:layer_count", this._onChangeLayerCount, this); + + }, + + /** + * Check event origin + */ + _checkOrigin: function(ev) { + // If the mouse down come from jspVerticalBar + // dont stop the propagation, but if the event + // is a touchstart, stop the propagation + var come_from_scroll = (($(ev.target).closest(".jspVerticalBar").length > 0) && (ev.type != "touchstart")); + + if (!come_from_scroll) { + ev.stopPropagation(); + } + }, + + _stopPropagation: function(ev) { + ev.stopPropagation(); + }, + + _onBackdropClick: function(e) { + + e.preventDefault(); + e.stopPropagation(); + + this.$el.find(".backdrop").fadeOut(250); + this.$el.find(".cartodb-attribution").fadeOut(250); + + }, + + _onAttributionClick: function(e) { + + e.preventDefault(); + e.stopPropagation(); + + this.$el.find(".backdrop").fadeIn(250); + this.$el.find(".cartodb-attribution").fadeIn(250); + + }, + + _toggle: function(e) { + + e.preventDefault(); + e.stopPropagation(); + + this.model.set("open", !this.model.get("open")); + + }, + + _toggleFullScreen: function(ev) { + + ev.stopPropagation(); + ev.preventDefault(); + + var doc = window.document; + var docEl = $("#map > div")[0]; + + var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen; + var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen; + + var mapView = this.options.mapView; + + if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement) { + + requestFullScreen.call(docEl); + + if (mapView) { + + mapView.options.map.set("scrollwheel", true); + + } + + } else { + + cancelFullScreen.call(doc); + + } + }, + + _open: function() { + + var right = this.$el.find(".aside").width(); + + this.$el.find(".cartodb-header").animate({ right: right }, 200) + this.$el.find(".aside").animate({ right: 0 }, 200) + this.$el.find(".cartodb-attribution-button").animate({ right: right + parseInt(this.$el.find(".cartodb-attribution-button").css("right")) }, 200) + this.$el.find(".cartodb-attribution").animate({ right: right + parseInt(this.$el.find(".cartodb-attribution-button").css("right")) }, 200) + this._initScrollPane(); + + }, + + _close: function() { + + this.$el.find(".cartodb-header").animate({ right: 0 }, 200) + this.$el.find(".aside").animate({ right: - this.$el.find(".aside").width() }, 200) + this.$el.find(".cartodb-attribution-button").animate({ right: 20 }, 200) + this.$el.find(".cartodb-attribution").animate({ right: 20 }, 200) + + }, + + default_options: { + timeout: 0, + msg: '' + }, + + _stopPropagation: function(ev) { + ev.stopPropagation(); + }, + + doOnOrientationChange: function() { + + switch(window.orientation) + { + case -90: + case 90: this.recalc("landscape"); + break; + default: this.recalc("portrait"); + break; + } + }, + + recalc: function(orientation) { + + var height = $(".legends > div.cartodb-legend-stack").height(); + + if (this.$el.hasClass("open") && height < 100 && !this.$el.hasClass("torque")) { + + this.$el.css("height", height); + this.$el.find(".top-shadow").hide(); + this.$el.find(".bottom-shadow").hide(); + + } else if (this.$el.hasClass("open") && height < 100 && this.$el.hasClass("legends") && this.$el.hasClass("torque")) { + + this.$el.css("height", height + $(".legends > div.torque").height() ); + this.$el.find(".top-shadow").hide(); + this.$el.find(".bottom-shadow").hide(); + + } + + }, + + _onChangeLayerCount: function() { + + var layer_count = this.model.get("layer_count"); + var msg = layer_count + " layer" + (layer_count != 1 ? "s" : ""); + this.$el.find(".aside .layer-container > h3").html(msg); + + }, + + _onChangeOpen: function() { + this.model.get("open") ? this._open() : this._close(); + }, + + _createLayer: function(_class, opts) { + return new cdb.geo.ui[_class](opts); + }, + + _getLayers: function() { + + this.layers = []; + + // we add the layers to the array depending on the method used + // to sent us the layers + if (this.options.layerView) { + this._getLayersFromLayerView(); + } else { + _.each(this.map.layers.models, this._getLayer, this); + } + + }, + + _getLayersFromLayerView: function() { + + if (this.options.layerView && this.options.layerView.model.get("type") == "layergroup") { + + this.layers = _.map(this.options.layerView.layers, function(l, i) { + + var m = new cdb.core.Model(l); + + m.set('order', i); + m.set('type', 'layergroup'); + m.set('visible', l.visible); + m.set('layer_name', l.options.layer_name); + + layerView = this._createLayer('LayerViewFromLayerGroup', { + model: m, + layerView: this.options.layerView, + layerIndex: i + }); + + return layerView.model; + + }, this); + + } else if (this.options.layerView && (this.options.layerView.model.get("type") == "torque")) { + + var layerView = this._createLayer('LayerView', { model: this.options.layerView.model }); + + this.layers.push(layerView.model); + + } + }, + + _getLayer: function(layer) { + + if (layer.get("type") == 'layergroup' || layer.get('type') === 'namedmap') { + + var layerGroupView = this.mapView.getLayerByCid(layer.cid); + + for (var i = 0 ; i < layerGroupView.getLayerCount(); ++i) { + + var l = layerGroupView.getLayer(i); + var m = new cdb.core.Model(l); + + m.set('order', i); + m.set('type', 'layergroup'); + m.set('visible', l.visible); + m.set('layer_name', l.options.layer_name); + + layerView = this._createLayer('LayerViewFromLayerGroup', { + model: m, + layerView: layerGroupView, + layerIndex: i + }); + + this.layers.push(layerView.model); + + } + + } else if (layer.get("type") === "CartoDB" || layer.get('type') === 'torque') { + + if (layer.get('type') === 'torque') { + layer.on("change:visible", this._toggleSlider, this); + } + + this.layers.push(layer); + + } + + }, + + _toggleSlider: function(m) { + + if (m.get("visible")) { + this.$el.addClass("with-torque"); + this.slider.show(); + } else { + this.$el.removeClass("with-torque"); + this.slider.hide(); + } + + }, + + _reInitScrollpane: function() { + this.$('.scrollpane').data('jsp') && this.$('.scrollpane').data('jsp').reinitialise(); + }, + + _bindOrientationChange: function() { + + var self = this; + + var onOrientationChange = function() { + $(".cartodb-mobile .scrollpane").css("max-height", self.$el.height() - 30); + $('.cartodb-mobile .scrollpane').data('jsp') && $('.cartodb-mobile .scrollpane').data('jsp').reinitialise(); + }; + + if (!window.addEventListener) { + window.attachEvent('orientationchange', onOrientationChange, this); + } else { + window.addEventListener('orientationchange', _.bind(onOrientationChange)); + } + + }, + + _renderOverlays: function() { + + var hasSearchOverlay = false; + var hasZoomOverlay = false; + var hasLoaderOverlay = false; + var hasLayerSelector = false; + + _.each(this.overlays, function(overlay) { + + if (!this.visibility_options.search && overlay.type == 'search') { + if (this.visibility_options.search !== false && this.visibility_options.search !== "false") { + this._addSearch(); + hasSearchOverlay = true; + } + } + + if (!this.visibility_options.zoomControl && overlay.type === 'zoom') { + if (this.visibility_options.zoomControl !== "false") { + this._addZoom(); + hasZoomOverlay = true; + } + } + + if (!this.visibility_options.loaderControl && overlay.type === 'loader') { + if (this.visibility_options.loaderControl !== "false") { + this._addLoader(); + hasLoaderOverlay = true; + } + } + + if (overlay.type == 'fullscreen' && !this.mobileEnabled) { + this._addFullscreen(); + } + + if (overlay.type == 'header') { + this._addHeader(overlay); + } + + if (overlay.type == 'layer_selector') { + hasLayerSelector = true; + } + + }, this); + + var search_visibility = this.visibility_options.search === true || this.visibility_options.search === "true"; + var zoom_visibility = this.visibility_options.zoomControl === true || this.visibility_options.zoomControl === "true"; + var loader_visibility = this.visibility_options.loaderControl === true || this.visibility_options.loaderControl === "true"; + var layer_selector_visibility = this.visibility_options.layer_selector; + + if (!hasSearchOverlay && search_visibility) this._addSearch(); + if (!hasZoomOverlay && zoom_visibility) this._addZoom(); + if (!hasLoaderOverlay && loader_visibility) this._addLoader(); + if (layer_selector_visibility || hasLayerSelector && layer_selector_visibility == undefined) this.hasLayerSelector = true; + + }, + + _initScrollPane: function() { + + if (this.$scrollpane) return; + + var self = this; + + var height = this.$el.height(); + this.$scrollpane = this.$el.find(".scrollpane"); + + setTimeout(function() { + self.$scrollpane.css("max-height", height - 60); + self.$scrollpane.jScrollPane({ showArrows: true }); + }, 500); + + }, + + _addZoom: function() { + + var template = cdb.core.Template.compile('\ + +\ + -\ +
          ', 'mustache' + ); + + var zoom = new cdb.geo.ui.Zoom({ + model: this.options.map, + template: template + }); + + this.$el.append(zoom.render().$el); + this.$el.addClass("with-zoom"); + + }, + + _addLoader: function() { + + var template = cdb.core.Template.compile('
          ', 'mustache'); + + this.loader = new cdb.geo.ui.TilesLoader({ + template: template + }); + + this.$el.append(this.loader.render().$el); + this.$el.addClass("with-loader"); + + }, + + _addFullscreen: function() { + + if (this.visibility_options.fullscreen != false) { + this.hasFullscreen = true; + this.$el.addClass("with-fullscreen"); + } + + }, + + _addSearch: function() { + + this.hasSearch = true; + + var template = cdb.core.Template.compile('\ +
          \ + \ + \ + \ +
          \ + ', 'mustache' + ); + + var search = new cdb.geo.ui.Search({ + template: template, + mapView: this.mapView, + model: this.mapView.map + }); + + this.$el.find(".aside").prepend(search.render().$el); + this.$el.find(".cartodb-searchbox").show(); + this.$el.addClass("with-search"); + + }, + + _addHeader: function(overlay) { + + this.hasHeader = true; + + this.$header = this.$el.find(".cartodb-header"); + + var title_template = _.template('
          <% if (show_title) { %>
          <%= title %>
          <% } %><% if (show_description) { %>
          <%= description %><% } %>
          '); + + var extra = overlay.options.extra; + var has_header = false; + var show_title = false, show_description = false; + + if (extra) { + + if (this.visibility_options.title || this.visibility_options.title != false && extra.show_title) { + has_header = true; + show_title = true; + } + + if (this.visibility_options.description || this.visibility_options.description != false && extra.show_description) { + has_header = true; + show_description = true; + } + + if (this.slides) { + has_header = true; + } + + var $hgroup = title_template({ + title: cdb.core.sanitize.html(extra.title), + show_title:show_title, + description: cdb.core.sanitize.html(extra.description), + show_description: show_description + }); + + if (has_header) { + this.$el.addClass("with-header"); + this.$header.find(".content").append($hgroup); + } + + } + + }, + + _addAttributions: function() { + + var attributions = ""; + + this.options.mapView.$el.find(".leaflet-control-attribution").hide(); // TODO: remove this from here + + if (this.options.layerView) { + + attributions = this.options.layerView.model.get("attribution"); + this.$el.find(".cartodb-attribution").append(attributions); + + } else if (this.options.map.get("attribution")) { + + attributions = this.options.map.get("attribution"); + + _.each(attributions, function(attribution) { + var $li = $("
        • "); + var $el = $li.html(attribution); + this.$el.find(".cartodb-attribution").append($li); + }, this); + + } + + if (attributions) { + this.$el.find(".cartodb-attribution-button").fadeIn(250); + } + + }, + + _renderLayers: function() { + + var hasLegendOverlay = this.visibility_options.legends; + + var legends = this.layers.filter(function(layer) { + return layer.get("legend") && layer.get("legend").type !== "none" + }); + + var hasLegends = legends.length ? true : false; + + if (!this.hasLayerSelector && !hasLegendOverlay) return; + if (!this.hasLayerSelector && !hasLegends) return; + if (this.layers.length == 0) return; + if (this.layers.length == 1 && !hasLegends) return; + + this.$el.addClass("with-layers"); + + this.model.set("layer_count", 0); + + if (!this.hasSearch) this.$el.find(".aside .layer-container").prepend("

          "); + + _.each(this.layers, this._renderLayer, this); + + }, + + _renderLayer: function(data) { + + var hasLegend = data.get("legend") && data.get("legend").type !== "" && data.get("legend").type !== "none"; + + // When the layer selector is disabled, don't show the layer if it doesn't have legends + if (!this.hasLayerSelector && !hasLegend) return; + if (!this.hasLayerSelector && !data.get("visible")) return; + + var hide_toggle = (this.layers.length == 1 || !this.hasLayerSelector); + + var show_legends = true; + + if (this.visibility_options && this.visibility_options.legends !== undefined) { + show_legends = this.visibility_options.legends; + } + + var layer = new cdb.geo.ui.MobileLayer({ + model: data, + show_legends: show_legends, + show_title: !this.hasLayerSelector ? false : true, + hide_toggle: hide_toggle + }); + + this.$el.find(".aside .layers").append(layer.render().$el); + + layer.bind("change_visibility", this._reInitScrollpane, this); + + this.model.set("layer_count", this.model.get("layer_count") + 1); + + }, + + _renderTorque: function() { + + if (this.options.torqueLayer) { + + this.hasTorque = true; + + this.slider = new cdb.geo.ui.TimeSlider({type: "time_slider", layer: this.options.torqueLayer, map: this.options.map, pos_margin: 0, position: "none" , width: "auto" }); + + this.slider.bind("time_clicked", function() { + this.slider.toggleTime(); + }, this); + + this.$el.find(".torque").append(this.slider.render().$el); + + if (this.options.torqueLayer.hidden) this.slider.hide(); + else this.$el.addClass("with-torque"); + } + + }, + + _renderSlidesController: function() { + + if (this.slides) { + + this.$el.addClass("with-slides"); + + this.slidesController = new cdb.geo.ui.SlidesController({ + show_counter: true, + transitions: this.options.transitions, + visualization: this.options.visualization, + slides: this.slides + }); + + this.$el.append(this.slidesController.render().$el); + + } + + }, + + render: function() { + + this._bindOrientationChange(); + + this.$el.html(this.template(this.options)); + + this.$header = this.$el.find(".cartodb-header"); + this.$header.show(); + + this._renderOverlays(); + + this._renderSlidesController(); + + this._addAttributions(); + + this._getLayers(); + this._renderLayers(); + this._renderTorque(); + + return this; + + } + +}); + +/** + * Show or hide tiles loader + * + * Usage: + * + * var tiles_loader = new cdb.geo.ui.TilesLoader(); + * mapWrapper.$el.append(tiles_loader.render().$el); + * + */ + + +cdb.geo.ui.TilesLoader = cdb.core.View.extend({ + + className: "cartodb-tiles-loader", + + default_options: { + animationSpeed: 500 + }, + + initialize: function() { + _.defaults(this.options, this.default_options); + this.isVisible = 0; + this.template = this.options.template ? this.options.template : cdb.templates.getTemplate('geo/tiles_loader'); + }, + + render: function() { + this.$el.html($(this.template(this.options))); + return this; + }, + + show: function(ev) { + if(this.isVisible) return; + if (!cdb.core.util.ie || (cdb.core.util.browser.ie && cdb.core.util.browser.ie.version >= 10)) { + this.$el.fadeTo(this.options.animationSpeed, 1) + } else { + this.$el.show(); + } + this.isVisible++; + }, + + hide: function(ev) { + this.isVisible--; + if(this.isVisible > 0) return; + this.isVisible = 0; + if (!cdb.core.util.ie || (cdb.core.util.browser.ie && cdb.core.util.browser.ie.version >= 10)) { + this.$el.stop(true).fadeTo(this.options.animationSpeed, 0) + } else { + this.$el.hide(); + } + }, + + visible: function() { + return this.isVisible > 0; + } + +}); + + +cdb.geo.ui.InfoBox = cdb.core.View.extend({ + + className: 'cartodb-infobox', + defaults: { + pos_margin: 20, + position: 'bottom|right', + width: 200 + }, + + initialize: function() { + var self = this; + _.defaults(this.options, this.defaults); + if(this.options.layer) { + this.enable(); + } + this.setTemplate(this.options.template || this.defaultTemplate, 'mustache'); + }, + + setTemplate: function(tmpl) { + this.template = cdb.core.Template.compile(tmpl, 'mustache'); + }, + + enable: function() { + if(this.options.layer) { + this.options.layer + .on('featureOver', function(e, latlng, pos, data) { + this.render(data).show(); + }, this) + .on('featureOut', function() { + this.hide(); + }, this); + } + }, + + disable: function() { + if(this.options.layer) { + this.options.layer.off(null, null, this); + } + }, + + // set position based on a string like "top|right", "top|left", "bottom|righ"... + setPosition: function(pos) { + var props = {}; + if(pos.indexOf('top') !== -1) { + props.top = this.options.pos_margin; + } else if(pos.indexOf('bottom') !== -1) { + props.bottom = this.options.pos_margin; + } + + if(pos.indexOf('left') !== -1) { + props.left = this.options.pos_margin; + } else if(pos.indexOf('right') !== -1) { + props.right = this.options.pos_margin; + } + this.$el.css(props); + + }, + + render: function(data) { + this.$el.html( this.template(data) ); + if(this.options.width) { + this.$el.css('width', this.options.width); + } + if(this.options.position) { + this.setPosition(this.options.position); + } + return this; + } + +}); + + + +cdb.geo.ui.Tooltip = cdb.geo.ui.InfoBox.extend({ + + defaultTemplate: '

          {{text}}

          ', + className: 'cartodb-tooltip', + + defaults: { + vertical_offset: 0, + horizontal_offset: 0, + position: 'top|center' + }, + + initialize: function() { + if(!this.options.mapView) { + throw new Error("mapView should be present"); + } + this.options.template = this.options.template || this.defaultTemplate; + cdb.geo.ui.InfoBox.prototype.initialize.call(this); + this._filter = null; + this.showing = false; + this.showhideTimeout = null; + }, + + setLayer: function(layer) { + this.options.layer = layer; + return this; + }, + + /** + * sets a filter to open the tooltip. If the feature being hovered + * pass the filter the tooltip is shown + * setFilter(null) removes the filter + */ + setFilter: function(f) { + this._filter = f; + return this; + }, + + setFields: function(fields) { + this.options.fields = fields; + return this; + }, + + setAlternativeNames: function(n) { + this.options.alternative_names = n; + }, + + enable: function() { + if(this.options.layer) { + // unbind previous events + this.options.layer.unbind(null, null, this); + this.options.layer + .on('mouseover', function(e, latlng, pos, data) { + + if (this.options.fields && this.options.fields.length > 0) { + + var non_valid_keys = ['fields', 'content']; + + if (this.options.omit_columns) { + non_valid_keys = non_valid_keys.concat(this.options.omit_columns); + } + + var c = cdb.geo.ui.InfowindowModel.contentForFields(data, this.options.fields, { + empty_fields: this.options.empty_fields + }); + + // Remove fields and content from data + // and make them visible for custom templates + data.content = _.omit(data, non_valid_keys); + + // loop through content values + data.fields = c.fields; + + // alternamte names + var names = this.options.alternative_names; + if (names) { + for(var i = 0; i < data.fields.length; ++i) { + var f = data.fields[i]; + f.title = names[f.title] || f.title; + } + } + this.show(pos, data); + this.showing = true; + } else if (this.showing) { + this.hide(); + this.showing = false; + } + }, this) + .on('mouseout', function() { + if (this.showing) { + this.hide(); + this.showing = false; + } + }, this); + this.add_related_model(this.options.layer); + } + }, + + disable: function() { + if(this.options.layer) { + this.options.layer.unbind(null, null, this); + } + this.hide(); + this.showing = false; + }, + + _visibility: function() { + var self = this; + clearTimeout(this.showhideTimeout); + this.showhideTimeout = setTimeout(self._showing ? + function() { self.$el.fadeIn(100); } + : + function() { self.$el.fadeOut(200); } + , 50); + }, + + hide: function() { + if (this._showing) { + this._showing = false; + this._visibility(); + } + }, + + show: function(pos, data) { + if (this._filter && !this._filter(data)) { + return this; + } + this.render(data); + //this.elder('show', pos, data); + this.setPosition(pos); + if (!this._showing) { + this._showing = true; + this._visibility(); + } + return this; + }, + + setPosition: function(point) { + var pos = this.options.position; + var height = this.$el.innerHeight(); + var width = this.$el.innerWidth(); + var mapViewSize = this.options.mapView.getSize(); + var top = 0; + var left = 0; + + // Vertically + if (pos.indexOf('top') !== -1) { + top = point.y - height; + } else if (pos.indexOf('middle') !== -1) { + top = point.y - (height/2); + } else { // bottom + top = point.y; + } + + // Fix vertical overflow + if (top < 0) { + top = point.y; + } else if (top + height > mapViewSize.y) { + top = point.y - height; + } + + // Horizontally + if(pos.indexOf('left') !== -1) { + left = point.x - width; + } else if(pos.indexOf('center') !== -1) { + left = point.x - (width/2); + } else { // right + left = point.x; + } + + // Fix horizontal overflow + if (left < 0) { + left = point.x; + } else if (left + width > mapViewSize.x) { + left = point.x - width; + } + + // Add offsets + top += this.options.vertical_offset; + left += this.options.horizontal_offset; + + this.$el.css({ + top: top, + left: left + }); + }, + + render: function(data) { + var sanitizedOutput = cdb.core.sanitize.html(this.template(data)); + this.$el.html( sanitizedOutput ); + return this; + } + +}); + +/** + * FullScreen widget: + * + * var widget = new cdb.ui.common.FullScreen({ + * doc: ".container", // optional; if not specified, we do the fullscreen of the whole window + * template: this.getTemplate("table/views/fullscreen") + * }); + * + */ + +cdb.ui.common.FullScreen = cdb.core.View.extend({ + + tagName: 'div', + className: 'cartodb-fullscreen', + + events: { + "click a": "_toggleFullScreen" + }, + + initialize: function() { + _.bindAll(this, 'render'); + _.defaults(this.options, this.default_options); + this._addWheelEvent(); + }, + + _addWheelEvent: function() { + var self = this; + var mapView = this.options.mapView; + + $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange', function() { + if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement && !document.msFullscreenElement) { + if (self.model.get("allowWheelOnFullscreen")) { + mapView.options.map.set("scrollwheel", false); + } + } + mapView.invalidateSize(); + }); + }, + + _toggleFullScreen: function(ev) { + if (ev) { + this.killEvent(ev); + } + + var doc = window.document; + var docEl = doc.documentElement; + + if (this.options.doc) { // we use a custom element + docEl = $(this.options.doc)[0]; + } + + var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen; + var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen; + var mapView = this.options.mapView; + + if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) { + if (docEl.webkitRequestFullScreen) { + // Cartodb.js #361 :: Full screen button not working on Safari 8.0.3 #361 + // Safari has a bug that fullScreen doestn't work with Element.ALLOW_KEYBOARD_INPUT); + // Reference: Ehttp://stackoverflow.com/questions/8427413/webkitrequestfullscreen-fails-when-passing-element-allow-keyboard-input-in-safar + requestFullScreen.call(docEl, undefined); + } else { + // CartoDB.js #412 :: Fullscreen button is throwing errors + // Nowadays (2015/03/25), fullscreen is not supported in iOS Safari. Reference: http://caniuse.com/#feat=fullscreen + if (requestFullScreen) { + requestFullScreen.call(docEl); + } + } + + if (mapView && this.model.get("allowWheelOnFullscreen")) { + mapView.map.set("scrollwheel", true); + } + } else { + cancelFullScreen.call(doc); + } + }, + + render: function() { + var options = _.extend( + this.options, + { + mapUrl: location.href || '' + } + ); + this.$el.html(this.options.template(options)); + + if (!this._canFullScreenBeEnabled()) { + this.undelegateEvents(); + cdb.log.info('FullScreen API is deprecated on insecure origins. See https://goo.gl/rStTGz for more details.'); + } + + return this; + }, + + _canFullScreenBeEnabled: function() { + if (this._isInIframe()) { + var parentUrl = document.referrer; + if (parentUrl.search('https:') !== 0) { + return false; + } + } + return true; + }, + + _isInIframe: function() { + try { + return window.self !== window.top; + } catch (e) { + return true; + } + } + +}); + + +/** + * Default widget view: + * + * It contains: + * - view model (viewModel) + * - data model (dataModel) + * + * It will offet to the user: + * - get current data (getData) + * - filter the current datasource (filter), each view will let + * different possibilities. + * - Sync or unsync widget (sync/unsync), making the proper view + * listen or not changes from the current datasource. + * + */ + +cdb.geo.ui.Widget.View = cdb.core.View.extend({ + + className: 'Widget Widget--light', + + options: { + columns_title: [], + sync: true + }, + + initialize: function() { + this.dataModel = this.model; + this.viewModel = new cdb.core.Model({ + title: this.model.get('options').title, + type: this.model.get('options').type, + sync: this.model.get('options').sync, + columns_title: this.model.get('options').columns_title + }); + }, + + render: function() { + this._initViews(); + return this; + }, + + _initViews: function() { + this._loader = new cdb.geo.ui.Widget.Loader({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + this.$el.append(this._loader.render().el); + this.addView(this._loader); + + this._error = new cdb.geo.ui.Widget.Error({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + this._error.bind('refreshData', function() { + console.log("refresh data man!"); + }, this); + this.$el.append(this._error.render().el); + this.addView(this._error); + + var content = this._createContentView(); + this.$el.append(content.render().el); + this.addView(content); + }, + + // Generate and return content view. + // In this case it will be the standard widget content. + _createContentView: function() { + return new cdb.geo.ui.Widget.Content({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + } +}); + + +cdb.geo.ui.Widget.Model = cdb.core.Model.extend({ + +}); + + + +cdb.geo.ui.Widget.Collection = Backbone.Collection.extend({ + + model: cdb.geo.ui.Widget.Model + +}); + +/** + * Default widget loader view: + * + * It will listen or not to dataModel changes when + * first load is done. + * + */ + +cdb.geo.ui.Widget.Loader = cdb.core.View.extend({ + + className: 'Widget-loader', + + initialize: function() { + this.dataModel = this.options.dataModel; + this.viewModel = this.options.viewModel; + this._initBinds(); + }, + + _initBinds: function() { + this.dataModel.once('loading', this.show, this); + this.dataModel.once('error', this._onFirstLoad, this); + this.dataModel.once('change:data', this._onFirstLoad, this); + this.viewModel.bind('change:sync', this._checkBinds, this); + this.add_related_model(this.dataModel); + this.add_related_model(this.viewModel); + }, + + _onFirstLoad: function() { + this.hide(); + this._unbindEvents(); // Remove any old dataModel binding + this._checkBinds(); + }, + + _checkBinds: function() { + var isSync = this.viewModel.get('sync'); + this[ isSync ? '_bindEvents' : '_unbindEvents'](); + }, + + _bindEvents: function() { + this.dataModel.bind('error change:data', this.hide, this); + this.dataModel.bind('loading', this.show, this); + }, + + _unbindEvents: function() { + this.dataModel.unbind(null, null, this); + }, + + show: function() { + this.$el.addClass('is-visible'); + }, + + hide: function() { + this.$el.removeClass('is-visible'); + } + +}); + +/** + * Default widget error view: + * + * It will listen or not to dataModel changes when + * first load is done. + * + */ + +cdb.geo.ui.Widget.Error = cdb.core.View.extend({ + + className: 'Widget-error', + + _TEMPLATE: ' ' + + '', + + events: { + 'click .js-refresh': '_onRefreshClick' + }, + + initialize: function() { + this.dataModel = this.options.dataModel; + this.viewModel = this.options.viewModel; + this._initBinds(); + }, + + render: function() { + var template = _.template(this._TEMPLATE); + this.$el.html(template()); + return this; + }, + + _initBinds: function() { + this.dataModel.once('error', function() { + this.show(); + this._onFirstLoad(); + }, this); + this.dataModel.once('change:data', this._onFirstLoad, this); + this.viewModel.bind('change:sync', this._checkBinds, this); + this.add_related_model(this.dataModel); + this.add_related_model(this.viewModel); + }, + + _onFirstLoad: function() { + this._unbindEvents(); // Remove any old dataModel binding + this._checkBinds(); + }, + + _checkBinds: function() { + var isSync = this.viewModel.get('sync'); + this[ isSync ? '_bindEvents' : '_unbindEvents'](); + }, + + _bindEvents: function() { + this.dataModel.bind('error', this.show, this); + this.dataModel.bind('loading change:data', this.hide, this); + }, + + _unbindEvents: function() { + this.dataModel.unbind(null, null, this); + }, + + _onRefreshClick: function() { + this.trigger('refreshData', this); + }, + + show: function() { + this.$el.css('display', 'flex'); + this.$el.addClass('is-visible'); + }, + + hide: function() { + var self = this; + this.$el.removeClass('is-visible'); + setTimeout(function() { + self.$el.hide(); + }, 500); + } + +}); + +/** + * Default widget content view: + * + */ + +cdb.geo.ui.Widget.Content = cdb.core.View.extend({ + + className: 'Widget-body', + + _TEMPLATE: ' ' + + '
          '+ + '
          '+ + '

          <%= title %>

          '+ + '
          '+ + '
          '+ + '
          <%= itemsCount %> items
          '+ + '
          '+ + '
          '+ + '
          ', + + _PLACEHOLDER: ' ' + + '
            ' + + '
          • ' + + '
          ', + + initialize: function() { + this.dataModel = this.options.dataModel; + this.viewModel = this.options.viewModel; + this._initBinds(); + }, + + render: function() { + this.clearSubViews(); + + var template = _.template(this._TEMPLATE); + var data = this.dataModel.getData(); + var isDataEmpty = _.isEmpty(data) || _.size(data) === 0; + this.$el.html( + template({ + title: this.viewModel.get('title'), + itemsCount: !isDataEmpty ? data.length : '-' + }) + ); + + if (isDataEmpty) { + this._addPlaceholder(); + } + + return this; + }, + + _initBinds: function() { + this.dataModel.once('error', this._onFirstLoad, this); + this.dataModel.once('change:data', this._onFirstLoad, this); + this.viewModel.bind('change:sync', this._checkBinds, this); + this.add_related_model(this.dataModel); + this.add_related_model(this.viewModel); + }, + + _onFirstLoad: function() { + this.render(); + this._unbindEvents(); // Remove any old dataModel binding + this._checkBinds(); + }, + + _checkBinds: function() { + var isSync = this.viewModel.get('sync'); + this[ isSync ? '_bindEvents' : '_unbindEvents'](); + }, + + _bindEvents: function() { + this.dataModel.bind('change:data', this.render, this); + }, + + _unbindEvents: function() { + this.dataModel.unbind(null, null, this); + }, + + _addPlaceholder: function() { + var placeholderTemplate = _.template(this._PLACEHOLDER); + this.$('.js-content').append(placeholderTemplate()); + } + +}); + +/** + * Histogram widget view + * + */ + +cdb.geo.ui.Widget.Histogram = {}; + +cdb.geo.ui.Widget.Histogram.View = cdb.geo.ui.Widget.View.extend({ + + _createContentView: function() { + return new cdb.geo.ui.Widget.Histogram.Content({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + } + +}); + +/** + * Default widget content view: + * + * + */ + +cdb.geo.ui.Widget.Histogram.Chart = cdb.core.View.extend({ + + defaults: { + duration: 750, + handleWidth: 6, + handleHeight: 23, + handleRadius: 3, + transitionType: 'elastic' + }, + + initialize: function() { + + _.bindAll(this, '_selectBars', '_adjustBrushHandles', '_onBrushMove', '_onBrushStart', '_onMouseMove', '_onMouseEnter', '_onMouseOut'); + + this._setupModel(); + this._setupDimensions(); + }, + + render: function() { + this._generateChart(); + + this._generateHorizontalLines(); + this._generateVerticalLines(); + + this._generateBars(); + + this._generateHandles(); + + this._setupBrush(); + this._generateXAxis(); + + return this; + }, + + _removeBars: function() { + this.chart.selectAll('.Bar').remove(); + }, + + _removeBrush: function() { + this.brush + .clear() + .event(this.chart.select('.Brush')); + this.chart.classed('is-selectable', false); + }, + + reset: function(data) { + this.loadData(data); + this._removeBrush(); + this.model.set({ a: 0, b: this.model.get('data').length }); + }, + + _generateVerticalLines: function() { + var range = d3.range(0, this.chartWidth + this.chartWidth / 4, this.chartWidth / 4); + + var lines = this.chart.select('.Lines'); + + lines.append('g') + .attr('class', 'y') + .selectAll('.x') + .data(range.slice(1, range.length - 1)) + .enter().append('svg:line') + .attr('y1', 0) + .attr('x1', function(d) { return d; }) + .attr('y2', this.chartHeight) + .attr('x2', function(d) { return d; }); + }, + + _generateHorizontalLines: function() { + var range = d3.range(0, this.chartHeight + this.chartHeight / 2, this.chartHeight / 2); + + var lines = this.chart.append('g') + .attr('class', 'Lines'); + + lines.append('g') + .attr('class', 'y') + .selectAll('.y') + .data(range) + .enter().append('svg:line') + .attr('class', 'y') + .attr('x1', 0) + .attr('y1', function(d) { return d; }) + .attr('x2', this.chartWidth) + .attr('y2', function(d) { return d; }); + + this.bottomLine = lines + .append('line') + .attr('class', 'l_bottom') + .attr('x1', 0) + .attr('y1', this.chartHeight) + .attr('x2', this.chartWidth - 1) + .attr('y2', this.chartHeight); + }, + + _setupModel: function() { + this.model = new cdb.core.Model({ data: this.options.data }); + this.model.bind('change:a change:b', this._onChangeRange, this); + this.model.bind('change:data', this._onChangeData, this); + this.model.bind('change:dragging', this._onChangeDragging, this); + }, + + _setupDimensions: function() { + var data = this.model.get('data'); + + this.margin = { top: 0, right: 10, bottom: 20, left: 10 }; + + this.canvasWidth = this.options.width; + this.canvasHeight = this.options.height; + + this.chartWidth = this.canvasWidth - this.margin.left - this.margin.right; + this.chartHeight = this.options.height; + + this._setupScales(); + }, + + _setupScales: function() { + var data = this.model.get('data'); + this.xScale = d3.scale.linear().domain([0, 100]).range([0, this.chartWidth]); + this.yScale = d3.scale.linear().domain([0, d3.max(data, function(d) { return d; } )]).range([this.chartHeight, 0]); + this.zScale = d3.scale.ordinal().domain(d3.range(data.length)).rangeRoundBands([0, this.chartWidth]); + }, + + _calcBarWidth: function() { + this.barWidth = this.chartWidth / this.model.get('data').length; + }, + + _generateChart: function() { + this.chart = d3.select(this.options.el[0]) + .selectAll('.Canvas') + .append('g') + .attr('class', 'Chart') + .attr('opacity', 0) + .attr('transform', 'translate(0, ' + this.options.y + ')'); + + this.chart.classed(this.options.className || '', true); + }, + + hide: function() { + this.chart + .transition() + .duration(150) + .attr('opacity', 0) + .attr('transform', 'translate(0, ' + (this.options.y - 10) + ')'); + }, + + show: function() { + this.chart + .attr('transform', 'translate(0, ' + (this.options.y + 10) + ')') + .transition() + .duration(150) + .attr('opacity', 1) + .attr('transform', 'translate(0, ' + (this.options.y) + ')'); + }, + + move: function() { + this.chart + .transition() + .duration(2500) + .attr('transform', 'translate(0, ' + (this.options.y + 90) + ')'); + }, + + _onBrushStart: function() { + this.chart.classed('is-selectable', true); + }, + + _selectBars: function() { + var self = this; + var extent = this.brush.extent(); + var lo = extent[0]; + var hi = extent[1]; + + this.model.set({ a: this._getLoBarIndex(), b: this._getHiBarIndex() }); + + this.chart.selectAll('.Bar').classed('is-selected', function(d, i) { + var a = Math.floor(i * self.barWidth); + var b = Math.floor(a + self.barWidth); + var LO = Math.floor(self.xScale(lo)); + var HI = Math.floor(self.xScale(hi)); + var isIn = (a > LO && a < HI) || (b > LO && b < HI) || (a <= LO && b >= HI); + return !isIn; + }); + }, + + _onChangeDragging: function() { + this.chart.classed('is-dragging', this.model.get('dragging')); + }, + + _onBrushMove: function() { + this.model.set({ dragging: true }); + this._selectBars(); + this._adjustBrushHandles(); + }, + + _onMouseEnter: function(d) { + }, + + _onMouseOut: function(d) { + var bars = this.chart.selectAll('.Bar'); + bars.classed('is-highlighted', false); + this.trigger('hover', { value: null }); + }, + + _onMouseMove: function(d) { + var x = d3.event.offsetX; + var a = Math.ceil(x / this.barWidth); + var data = this.model.get('data'); + + var format = d3.format("0,000"); + var bar = this.chart.select('.Bar:nth-child(' + a + ')'); + + if (bar && bar.node() && !bar.classed('is-selected')) { + var left = ((a - 1) * this.barWidth); + if (!this._isDragging()) { + this.trigger('hover', { left: left, value: data[a - 1] }); + } + } else { + this.trigger('hover', { value: null }); + } + + this.chart.selectAll('.Bar') + .classed('is-highlighted', false); + + if (bar && bar.node()) { + bar.classed('is-highlighted', true); + } + }, + + _isDragging: function() { + return this.model.get('dragging'); + }, + + selectRange: function(a, b) { + var data = this.model.get('data'); + var start = a * (100 / data.length); + var end = b * (100 / data.length); + + this.chart.select('.Brush').transition() + .duration(this.brush.empty() ? 0 : 100) + .call(this.brush.extent([start, end])) + .call(this.brush.event); + }, + + _selectRange: function(start, end) { + this.chart.select('.Brush').transition() + .duration(this.brush.empty() ? 0 : 150) + .call(this.brush.extent([start, end])) + .call(this.brush.event); + }, + + _getLoBarIndex: function() { + var extent = this.brush.extent(); + return Math.round(this.xScale(extent[0]) / this.barWidth); + }, + + _getHiBarIndex: function() { + var extent = this.brush.extent(); + return Math.round(this.xScale(extent[1]) / this.barWidth); + }, + + _getBarIndex: function() { + var x = d3.event.sourceEvent.offsetX - this.margin.left; + return Math.ceil(x / this.barWidth); + }, + + _setupBrush: function() { + var self = this; + + var xScale = this.xScale; + var brush = this.brush = d3.svg.brush().x(this.xScale); + + function onBrushEnd() { + var data = self.model.get('data'); + var a, b; + + self.model.set({ dragging: false }); + + if (brush.empty()) { + self.chart.selectAll('.Bar').classed('is-selected', false); + d3.select(this).call(brush.extent([0, 0])); + } else { + + var loBarIndex = self._getLoBarIndex(); + var hiBarIndex = self._getHiBarIndex(); + + a = loBarIndex * (100 / data.length); + b = hiBarIndex * (100 / data.length); + + if (!d3.event.sourceEvent) { + return; + } + + self._selectRange(a, b); + self.model.set({ a: loBarIndex, b: hiBarIndex }); + self._adjustBrushHandles(); + self._selectBars(); + + self.trigger('on_brush_end', self.model.get('a'), self.model.get('b')); + } + + if (d3.event.sourceEvent && a === undefined && b === undefined) { + var barIndex = self._getBarIndex(); + a = (barIndex - 1) * (100 / data.length); + b = (barIndex) * (100 / data.length); + self.model.set({ a: barIndex - 1, b: barIndex }); + self._selectRange(a, b); + } + } + + var data = this.model.get('data'); + + this.brush + .on('brushstart', this._onBrushStart) + .on('brush', this._onBrushMove) + .on('brushend', onBrushEnd); + + this.chart.append('g') + .attr('class', 'Brush') + .call(this.brush) + .selectAll('rect') + .attr('y', 0) + .attr('height', this.chartHeight) + .on('mouseenter', this._onMouseEnter) + .on('mouseout', this._onMouseOut) + .on('mousemove', this._onMouseMove); + }, + + _adjustBrushHandles: function() { + var extent = this.brush.extent(); + var lo = extent[0]; + var hi = extent[1]; + + this.leftHandleLine + .attr('x1', this.xScale(lo)) + .attr('x2', this.xScale(lo)); + + this.rightHandleLine + .attr('x1', this.xScale(hi)) + .attr('x2', this.xScale(hi)); + + if (this.options.handles) { + this.leftHandle + .attr('x', this.xScale(lo) - this.defaults.handleWidth / 2); + + this.rightHandle + .attr('x', this.xScale(hi) - this.defaults.handleWidth / 2); + } + }, + + _generateHandle: function() { + var handle = { width: this.defaults.handleWidth, height: this.defaults.handleHeight, radius: this.defaults.handleRadius }; + var yPos = (this.chartHeight / 2) - (this.defaults.handleHeight / 2); + + return this.chart.select('.Handles').append('rect') + .attr('class', 'Handle') + .attr('transform', 'translate(0, ' + yPos + ')') + .attr('width', handle.width) + .attr('height', handle.height) + .attr('rx', handle.radius) + .attr('ry', handle.radius); + }, + + _generateHandleLine: function() { + return this.chart.select('.Handles').append('line') + .attr('class', 'HandleLine') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', 0) + .attr('y2', this.chartHeight); + }, + + _removeHandles: function() { + this.chart.select('.Handles').remove(); + }, + + _generateHandles: function() { + this.chart.append('g').attr('class', 'Handles'); + this.leftHandleLine = this._generateHandleLine(); + this.rightHandleLine = this._generateHandleLine(); + + if (this.options.handles) { + this.leftHandle = this._generateHandle(); + this.rightHandle = this._generateHandle(); + } + }, + + _removeXAxis: function() { + d3.select('.axis').remove(); + }, + + _generateXAxis: function() { + var data = this.model.get('data'); + + var format = d3.format('0,000'); + + var xAxis = d3.svg.axis() + .scale(this.zScale) + .orient('bottom') + .innerTickSize(0) + .tickFormat(function(d, i) { + function calculateBins(n) { + if (n % 2 === 0) return 4; + else return 4; + } + + var p = Math.round(data.length / calculateBins(data.length)); + var v = i % p; + + if (v === 0 || i === 0 || i === (data.length - 1)) { + var sum = _.reduce(data.slice(0, i + 1), function(t, j) { + return j + t; + }); + return format(sum); + } else { + return ''; + } + }); + + this.chart.append('g') + .attr('class', 'axis') + .attr('transform', 'translate(0,' + (this.chartHeight + 5) + ')') + .call(xAxis); + }, + + refreshData: function(data, a, b) { + if (data && data.length > 0) { + this.model.set({ data: data, a: a, b: data.length - 1 }); + } + }, + + loadData: function(data) { + this.model.set({ a: 0, b: 0 }, { silent: true }); + this.model.set('data', data); + this._onChangeData(); + }, + + _onChangeData: function() { + this._removeBrush(); + this._removeBars(); + this._removeHandles(); + + this._setupDimensions(); + this._generateBars(); + this._generateHandles(); + + this._removeXAxis(); + this._generateXAxis(); + + this._setupBrush(); + }, + + _generateBars: function() { + var self = this; + var data = this.model.get('data'); + + this._calcBarWidth(); + + var bars = this.chart.append('g') + .attr('class', 'Bars') + .selectAll('.Bar') + .data(data); + + bars + .enter() + .append('rect') + .attr('class', 'Bar') + .attr('data', function(d) { return d; }) + .attr('transform', function(d, i) { + return 'translate(' + (i * self.barWidth) + ', 0 )'; + }) + .attr('y', self.chartHeight) + .attr('height', 0) + .attr('width', this.barWidth - 1); + + bars.transition() + .ease(this.defaults.transitionType) + .duration(self.defaults.duration) + .delay(function(d, i) { + return Math.random() * (100 + i * 10); + }) + .attr('height', function(d) { + return d ? self.chartHeight - self.yScale(d) : 0; + }) + .attr('y', function(d) { + return d ? self.yScale(d) : self.chartHeight; + }); + }, + + _onChangeRange: function() { + if (this.model.get('a') === 0 && this.model.get('b') === 0) { + return; + } + this.trigger('range_updated', this.model.get('a'), this.model.get('b')); + }, + + _formatNumber: function(value, unit) { + var format = d3.format("0,000"); + return format(value + unit ? ' ' + unit : ''); + }, + +}); + +cdb.geo.ui.Widget.Histogram.Content = cdb.geo.ui.Widget.Content.extend({ + + defaults: { + chartHeight: 48 + }, + + events: { + 'click .js-clear': '_reset', + 'click .js-zoom': '_zoom' + }, + + _TEMPLATE: ' ' + + '
          '+ + '
          '+ + '

          <%= title %>

          '+ + '
          '+ + '
          '+ + '
          0 NULL ROWS
          '+ + '
          0 MIN
          '+ + '
          0 AVG
          '+ + '
          0 MAX
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '

          '+ + '
          '+ + ' '+ + ' '+ + '
          '+ + '
          '+ + ' ', + + _PLACEHOLDER: ' ' + + '
            ' + + '
          • ' + + '
          • ' + + '
          • ' + + '
          • ' + + '
          ', + + _initViews: function() { + this._generateData(); + this._setupDimensions(); + this._generateCanvas(); + this._renderMainChart(); + this._renderMiniChart(); + }, + + render: function() { + + this.clearSubViews(); + + var template = _.template(this._TEMPLATE); + var data = this.dataModel.getData(); + var isDataEmpty = _.isEmpty(data) || _.size(data) === 0; + + this.$el.html( + template({ + title: this.viewModel.get('title'), + itemsCount: !isDataEmpty ? data.length : '-' + }) + ); + + if (isDataEmpty) { + this._addPlaceholder(); + } else { + this._setupBindings(); + this._initViews(); + } + + return this; + }, + + _renderMainChart: function() { + this.chart = new cdb.geo.ui.Widget.Histogram.Chart(({ + el: this.$('.js-chart'), + y: 0, + handles: true, + width: this.canvasWidth, + height: this.defaults.chartHeight, + data: this.dataModel.get('data') + })); + + this.chart.bind('range_updated', this._onRangeUpdated, this); + this.chart.bind('hover', this._onValueHover, this); + this.chart.render().show(); + + this._updateStats(); + }, + + _renderMiniChart: function() { + this.miniChart = new cdb.geo.ui.Widget.Histogram.Chart(({ + className: 'mini', + el: this.$('.js-chart'), + handles: false, + width: this.canvasWidth, + y: 90, + height: 20, + data: this.dataModel.get('data') + })); + + this.miniChart.bind('on_brush_end', this._onMiniRangeUpdated, this); + + this.miniChart.render(); + }, + + _setupBindings: function() { + this.viewModel.bind('change:zoom_enabled', this._onChangeZoomEnabled, this); + this.viewModel.bind('change:total', this._onChangeTotal, this); + this.viewModel.bind('change:max', this._onChangeMax, this); + this.viewModel.bind('change:min', this._onChangeMin, this); + this.viewModel.bind('change:avg', this._onChangeAvg, this); + }, + + _setupDimensions: function() { + this.margin = { top: 0, right: 10, bottom: 20, left: 10 }; + + this.canvasWidth = this.$('.js-chart').width(); + this.canvasHeight = this.defaults.chartHeight + this.margin.top + this.margin.bottom; + }, + + _onValueHover: function(info) { + var $tooltip = this.$(".js-tooltip"); + if (info.value) { + $tooltip.css({ top: 0, left: info.left }); + $tooltip.text(info.value); + $tooltip.show(); + } else { + $tooltip.hide(); + } + }, + + _onMiniRangeUpdated: function(a, b) { + this.viewModel.set({ a: a, b: b }); + var data = this._getData(); + var self = this; + + var refreshData = _.debounce(function() { + self.chart.refreshData(data, a, b); + self._updateStats(); + }, 100); + + refreshData(); + }, + + _onRangeUpdated: function(a, b) { + this.$(".js-filter").animate({ opacity: 1 }, 250); + this.viewModel.set({ a: a, b: b }); + this._updateStats(); + }, + + _onChangeZoomEnabled: function() { + this.$(".js-zoom").toggleClass('is-hidden', !this.viewModel.get('zoom_enabled')); + }, + + _onChangeTotal: function() { + this._animateValue('.js-val', 'total', ' SELECTED'); + }, + + _onChangeMax: function() { + this._animateValue('.js-max', 'max', 'MAX'); + }, + + _onChangeMin: function() { + this._animateValue('.js-min', 'min', 'MIN'); + }, + + _onChangeAvg: function() { + this._animateValue('.js-avg', 'avg', 'AVG'); + }, + + _generateData: function() { + var data = _.map(d3.range(Math.round(Math.random() * 80) + 2), function(d) { + return Math.round(Math.random() * 1000); + }); + + this.dataModel.set('data', data); + }, + + _animateValue: function(className, what, unit) { + var self = this; + var format = d3.format("0,000"); + + var from = this.viewModel.previous(what) || 0; + var to = this.viewModel.get(what); + + if (!to) return; + + $(className).prop('counter', from).stop().animate({ counter: to }, { + duration: 500, + easing: 'swing', + step: function (i) { + $(this).text(format(Math.floor(i)) + ' ' + unit); + } + }); + }, + + _getData: function(full) { + var data = this.dataModel.get('data'); + if (full) { + return data; + } + return data.slice(this.viewModel.get('a'), this.viewModel.get('b')); + }, + + _updateStats: function() { + var data = this._getData(); + var sum = _.reduce(data, function(t, j) { + return j + t; + }); + + var max = d3.max(data); + var avg = Math.round(d3.mean(data)); + var min = d3.min(data); + + this.viewModel.set({ total: sum, min: min, max: max, avg: avg }); + }, + + _zoom: function() { + this._expand(); + this.viewModel.set({ zoom_enabled: false }); + this.chart.loadData(this._getData()); + this.miniChart.selectRange(this.viewModel.get('a'), this.viewModel.get('b')); + this.miniChart.show(); + }, + + _reset: function() { + this._contract(); + this.viewModel.set({ zoom_enabled: true, a: 0, b: 100 }); + this.chart.reset(this._getData()); + this.$(".js-filter").animate({ opacity: 0 }, 0); + this.miniChart.hide(); + }, + + _contract: function() { + this.canvas + .attr('height', this.canvasHeight); + }, + + _expand: function() { + this.canvas + .attr('height', this.canvasHeight + 60); + }, + + _generateCanvas: function() { + this.canvas = d3.select(this.$el.find('.js-chart')[0]) + .attr('width', this.canvasWidth) + .attr('height', this.canvasHeight) + + this.canvas + .append('g') + .attr('class', 'Canvas'); + + this.canvas + .attr('transform', 'translate(10, 0)'); + } +}); + +cdb.geo.ui.Widget.HistogramModel = cdb.geo.ui.Widget.Model.extend({ + + options: { + page: 0, + per_page: 100 + }, + + defaults: { + data: [], + columns: [] + }, + + url: function() { + // TODO: Change this once the histogram endpoint is ready! + return this.get('dashboardBaseURL') + '/list/' + this.get('id'); + }, + + initialize: function() { + this._data = new Backbone.Collection(this.get('data')); + this._initBinds(); + }, + + _initBinds: function() { + this.bind('change:dashboardBaseURL', function(){ + var self = this; + this.fetch({ + error: function() { + self.trigger('error'); + } + }); + }, this); + }, + + _createUrlOptions: function() { + return _.compact(_(this.options).map( + function(v, k) { + return k + "=" + encodeURIComponent(v); + } + )).join('&'); + }, + + getData: function() { + return this._data; + }, + + getSize: function() { + return this._data.size(); + }, + + getDataSerialized: function() { + return this.get('data'); + }, + + fetch: function(opts) { + this.trigger("loading", this); + return cdb.core.Model.prototype.fetch.call(this,opts); + }, + + parse: function(r) { + this._data.reset(r); + return { + data: r.data + }; + } + +}); + + +/** + * List widget view + * + */ + +cdb.geo.ui.Widget.List = {}; + +cdb.geo.ui.Widget.List.View = cdb.geo.ui.Widget.View.extend({ + + _createContentView: function() { + return new cdb.geo.ui.Widget.List.Content({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + } + +}); + +/** + * Default widget content view: + * + * + */ + +cdb.geo.ui.Widget.List.Content = cdb.geo.ui.Widget.Content.extend({ + + _TEMPLATE: ' ' + + '
          '+ + '
          '+ + '

          <%= title %>

          '+ + '
          '+ + '
          '+ + '
          <%= itemsCount %> rows
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '', + + _PLACEHOLDER: ' ' + + '
            ' + + '
          • ' + + '
          • ' + + '
          • ' + + '
          • ' + + '
          ', + + render: function() { + this.clearSubViews(); + + var template = _.template(this._TEMPLATE); + var data = this.dataModel.getData(); + var isDataEmpty = _.isEmpty(data) || _.size(data) === 0; + this.$el.html( + template({ + title: this.viewModel.get('title'), + itemsCount: !isDataEmpty ? data.length : '-' + }) + ); + + if (isDataEmpty) { + this._addPlaceholder(); + } else { + this._initViews(); + } + + return this; + }, + + _initViews: function() { + var count = this.dataModel.getSize(); + + // List view -> items view + var list = new cdb.geo.ui.Widget.List.ItemsView({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + this.$('.js-content').html(list.render().el); + this.addView(list); + + var isScrollList = (list.$el.get(0).scrollHeight - list.$el.outerHeight()) > 0; + + if (count > 4) { + // Paginator + var pagination = new cdb.geo.ui.Widget.List.PaginatorView({ + $target: list.$el + }); + this.$('.js-footer').append(pagination.render().el); + this.addView(pagination); + } + + // Edges + if (isScrollList) { + var edges = new cdb.geo.ui.Widget.List.EdgesView({ + $target: list.$el + }); + this.$('.js-content').append(edges.render().el); + this.addView(edges); + } + } + +}); + +cdb.geo.ui.Widget.List.PaginatorView = cdb.core.View.extend({ + + className: 'Widget-nav Widget-contentSpaced', + + _TEMPLATE: ' ' + + '' + + '
          '+ + ''+ + ''+ + '
          ', + + events: { + 'click .js-up': '_scrollUp', + 'click .js-down': '_scrollDown' + }, + + initialize: function() { + if (!this.options.$target) { + throw new Error('target should be defined in order to be able to paginate'); + } + this._$target = this.options.$target; + this._scrollHeight = this._$target.get(0).scrollHeight - this._$target.outerHeight(); + this._initBinds(); + }, + + render: function() { + var template = _.template(this._TEMPLATE); + this.$el.html(template()); + this._checkScroll(); + return this; + }, + + _initBinds: function() { + var self = this; + this._$target.bind('scroll', function() { + self._checkScroll(); + }); + }, + + _unbindScroll: function() { + this._$target.unbind('scroll'); + }, + + _checkScroll: function() { + var currentScroll = this._$target.scrollTop(); + this.$('.js-up').toggleClass('is-disabled', currentScroll === 0); + this.$('.js-down').toggleClass('is-disabled', currentScroll >= this._scrollHeight); + }, + + _getEdgeVisibleItems: function() { + var areaHeight = this._$target.outerHeight(); + var firstEl = null; + var lastEl = null; + var items = this._$target.children('.Widget-listItem'); + + items.each(function(index, value) { + var top = $(this).position().top; + var height = $(this).outerHeight(); + + if (top > -1 && firstEl === null){ //first entirely visible element + firstEl = this; + } else if ((top + height) > areaHeight && lastEl === null){ + lastEl = items[index-1];//the last entirely visible was the element before + } + }); + + return [firstEl,lastEl]; + }, + + _scrollDown: function() { + var lastVisibleItem = this._getEdgeVisibleItems()[1]; + var currentScroll = this._$target.scrollTop(); + var $next = $(lastVisibleItem).next(); + if ($next.length) { + var top = $next.position().top; + var height = $next.outerHeight(); + var scrollPos = top + height - this._$target.outerHeight() + currentScroll; + if (scrollPos > 0) { + this._$target.scrollTop(scrollPos); + } + } + }, + + _scrollUp: function() { + var firstVisibleItem = this._getEdgeVisibleItems()[0]; + var currentScroll = this._$target.scrollTop(); + var $prev = $(firstVisibleItem).prev(); + if ($prev.length) { + var top = $prev.position().top; + var scrollPos = currentScroll + top; + this._$target.scrollTop(scrollPos); + } + }, + + clean: function() { + this._unbindScroll(); + cdb.core.View.prototype.clean.call(this); + } + +}); + +/** + * List edges view: + * + * - It shows the borders and the shadows, if needed. + * + */ + +cdb.geo.ui.Widget.List.EdgesView = cdb.core.View.extend({ + + _TEMPLATE: ' ' + + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          '+ + '
          ', + + initialize: function() { + this._$target = this.options.$target; + this._initBinds(); + }, + + render: function() { + this.clearSubViews(); + var template = _.template(this._TEMPLATE); + this.$el.html(template()); + this._checkScroll(); + return this; + }, + + _initBinds: function() { + var self = this; + this._$target.bind('scroll', function() { + self._checkScroll(); + }); + }, + + _unbindScroll: function() { + this._$target.unbind('scroll'); + }, + + _checkScroll: function() { + var currentScroll = this._$target.scrollTop(); + var maxScroll = this._$target.get(0).scrollHeight - this._$target.outerHeight(); + this.$('.js-topShadow').toggle(currentScroll !== 0); + this.$('.js-bottomShadow').toggle(currentScroll !== maxScroll); + }, + + clean: function() { + this._unbindScroll(); + cdb.core.View.prototype.clean.call(this); + } + +}); + +cdb.geo.ui.Widget.List.ItemView = cdb.core.View.extend({ + + tagName: 'li', + className: 'Widget-listItem', + + events: { + 'click .js-button': '_onItemClick' + }, + + _TEMPLATE: ' ' + + '<% if (isClickable) { %>'+ + ''+ + '<% } else { %>'+ + '
          '+ + '<% } %>', + + initialize: function() { + this.viewModel = this.options.viewModel; + }, + + render: function() { + var template = _.template(this._TEMPLATE); + var data = this.model.toJSON(); + var hasInteractivity = this._hasInteractivity(data); + var items = this._sanitizeData(data); + + this.$el.html( + template({ + items: items, + isClickable: hasInteractivity, + itemsCount: _.size(items) + }) + ); + + // If there is no cartodb_id defined, click event should + // be disabled + this[ hasInteractivity ? 'delegateEvents' : 'undelegateEvents' ](); + return this; + }, + + // Remove cartodb_id, if exists + // Replace titles if there are alternatives + // Convert data object to array items + _sanitizeData: function(data) { + var data = _.omit(data, function(value, key, object) { + return key === 'cartodb_id'; + }); + + // Convert to pair items and check if there is a column title + var arr = []; + var i = 0; + var columnTitles = this.viewModel.get('columns_title'); + + _.each(data, function(value, key) { + var title = columnTitles[i] || key; + arr.push([ title, value ]); + ++i; + }); + + return arr; + }, + + _hasInteractivity: function(data) { + return !_.isEmpty( + _.filter(data, function(value, key){ + return key === 'cartodb_id' + }) + ) + }, + + _onItemClick: function() { + console.log("on item click!"); + this.trigger('itemClicked', this.model, this); + } + +}); + +cdb.geo.ui.Widget.List.ItemsView = cdb.geo.ui.Widget.View.extend({ + + className: 'Widget-list js-list', + tagName: 'ul', + + events: { + 'scroll': '_checkScroll' + }, + + initialize: function() { + this.dataModel = this.options.dataModel; + this.viewModel = this.options.viewModel; + }, + + render: function() { + this.clearSubViews(); + this._renderList(); + return this; + }, + + _renderList: function() { + this.dataModel.getData().each(this._addItem, this); + }, + + _addItem: function(mdl) { + var v = new cdb.geo.ui.Widget.List.ItemView({ + model: mdl, + viewModel: this.viewModel + }) + this.addView(v); + this.$el.append(v.render().el); + } + +}); + +cdb.geo.ui.Widget.ListModel = cdb.geo.ui.Widget.Model.extend({ + + options: { + page: 0, + per_page: 100 + }, + + defaults: { + data: [], + columns: [] + }, + + url: function() { + return this.get('dashboardBaseURL') + '/list/' + this.get('id'); + }, + + initialize: function() { + this._data = new Backbone.Collection(this.get('data')); + this._initBinds(); + }, + + _initBinds: function() { + this.bind('change:dashboardBaseURL', function(){ + var self = this; + this.fetch({ + error: function() { + self.trigger('error'); + } + }); + }, this); + }, + + _createUrlOptions: function() { + return _.compact(_(this.options).map( + function(v, k) { + return k + "=" + encodeURIComponent(v); + } + )).join('&'); + }, + + getData: function() { + return this._data; + }, + + getSize: function() { + return this._data.size(); + }, + + getDataSerialized: function() { + return this.get('data'); + }, + + fetch: function(opts) { + this.trigger("loading", this); + return cdb.core.Model.prototype.fetch.call(this,opts); + }, + + parse: function(r) { + this._data.reset(r); + return { + data: r.data + }; + } +}); + +function SubLayerFactory() {}; + +SubLayerFactory.createSublayer = function(type, layer, position) { + type = type && type.toLowerCase(); + if (!type || type === 'mapnik' || type === 'cartodb') { + return new CartoDBSubLayer(layer, position); + } else if (type === 'http') { + return new HttpSubLayer(layer, position); + } else { + throw 'Sublayer type not supported'; + } +}; + +function SubLayerBase(_parent, position) { + this._parent = _parent; + this._position = position; + this._added = true; +} + +SubLayerBase.prototype = { + + toJSON: function() { + throw 'toJSON must be implemented'; + }, + + isValid: function() { + throw 'isValid must be implemented'; + }, + + remove: function() { + this._check(); + this._parent.removeLayer(this._position); + this._added = false; + this.trigger('remove', this); + this._onRemove(); + }, + + _onRemove: function() {}, + + toggle: function() { + this.get('hidden') ? this.show() : this.hide(); + return !this.get('hidden'); + }, + + show: function() { + if(this.get('hidden')) { + this.set({ + hidden: false + }); + } + }, + + hide: function() { + if(!this.get('hidden')) { + this.set({ + hidden: true + }); + } + }, + + set: function(new_attrs) { + this._check(); + var def = this._parent.getLayer(this._position); + var attrs = def.options; + for(var i in new_attrs) { + attrs[i] = new_attrs[i]; + } + this._parent.setLayer(this._position, def); + if (new_attrs.hidden !== undefined) { + this.trigger('change:visibility', this, new_attrs.hidden); + } + return this; + }, + + unset: function(attr) { + var def = this._parent.getLayer(this._position); + delete def.options[attr]; + this._parent.setLayer(this._position, def); + }, + + get: function(attr) { + this._check(); + var attrs = this._parent.getLayer(this._position); + return attrs.options[attr]; + }, + + isVisible: function(){ + return ! this.get('hidden'); + }, + + _check: function() { + if(!this._added) throw "sublayer was removed"; + }, + + _unbindInteraction: function() { + if(!this._parent.off) return; + this._parent.off(null, null, this); + }, + + _bindInteraction: function() { + if(!this._parent.on) return; + var self = this; + // binds a signal to a layer event and trigger on this sublayer + // in case the position matches + var _bindSignal = function(signal, signalAlias) { + signalAlias = signalAlias || signal; + self._parent.on(signal, function() { + var args = Array.prototype.slice.call(arguments); + if (parseInt(args[args.length - 1], 10) == self._position) { + self.trigger.apply(self, [signalAlias].concat(args)); + } + }, self); + }; + _bindSignal('featureOver'); + _bindSignal('featureOut'); + _bindSignal('featureClick'); + _bindSignal('layermouseover', 'mouseover'); + _bindSignal('layermouseout', 'mouseout'); + }, + + _setPosition: function(p) { + this._position = p; + } +}; + +// give events capabilitues +_.extend(SubLayerBase.prototype, Backbone.Events); + + +// CartoDB / Mapnik sublayers +function CartoDBSubLayer(layer, position) { + SubLayerBase.call(this, layer, position); + this._bindInteraction(); + + var layer = this._parent.getLayer(this._position); + // TODO: Test this + if (Backbone.Model && layer) { + this.infowindow = new Backbone.Model(layer.infowindow); + this.infowindow.bind('change', function() { + layer.infowindow = this.infowindow.toJSON(); + this._parent.setLayer(this._position, layer); + }, this); + } +}; + +CartoDBSubLayer.prototype = _.extend({}, SubLayerBase.prototype, { + + toJSON: function() { + var json = { + type: 'cartodb', + options: { + sql: this.getSQL(), + cartocss: this.getCartoCSS(), + cartocss_version: this.get('cartocss_version') || '2.1.0' + } + }; + + var interactivity = this.getInteractivity(); + if (interactivity && interactivity.length > 0) { + json.options.interactivity = interactivity; + var attributes = this.getAttributes(); + if (attributes.length > 0) { + json.options.attributes = { + id: 'cartodb_id', + columns: attributes + } + } + } + + if (this.get('raster')) { + json.options.raster = true; + json.options.geom_column = "the_raster_webmercator"; + json.options.geom_type = "raster"; + json.options.raster_band = this.get('raster_band') || 0; + // raster needs 2.3.0 to work + json.options.cartocss_version = this.get('cartocss_version') || '2.3.0'; + } + return json; + }, + + isValid: function() { + return this.get('sql') && this.get('cartocss'); + }, + + _onRemove: function() { + this._unbindInteraction(); + }, + + setSQL: function(sql) { + return this.set({ + sql: sql + }); + }, + + setCartoCSS: function(cartocss) { + return this.set({ + cartocss: cartocss + }); + }, + + setInteractivity: function(fields) { + return this.set({ + interactivity: fields + }); + }, + + setInteraction: function(active) { + this._parent.setInteraction(this._position, active); + }, + + getSQL: function() { + return this.get('sql'); + }, + + getCartoCSS: function() { + return this.get('cartocss'); + }, + + getInteractivity: function() { + var interactivity = this.get('interactivity'); + if (interactivity) { + if (typeof(interactivity) === 'string') { + interactivity = interactivity.split(','); + } + return this._trimArrayItems(interactivity); + } + }, + + getAttributes: function() { + var columns = []; + if (this.get('attributes')) { + columns = this.get('attributes'); + } else { + columns = _.map(this.infowindow.get('fields'), function(field){ + return field.name; + }); + } + return this._trimArrayItems(columns); + }, + + _trimArrayItems: function(array) { + return _.map(array, function(item) { + return item.trim(); + }) + } +}); + +// Http sublayer + +function HttpSubLayer(layer, position) { + SubLayerBase.call(this, layer, position); +}; + +HttpSubLayer.prototype = _.extend({}, SubLayerBase.prototype, { + + toJSON: function() { + var json = { + type: 'http', + options: { + urlTemplate: this.getURLTemplate() + } + }; + + var subdomains = this.get('subdomains'); + if (subdomains) { + json.options.subdomains = subdomains; + } + + var tms = this.get('tms'); + if (tms !== undefined) { + json.options.tms = tms; + } + return json; + }, + + isValid: function() { + return this.get('urlTemplate'); + }, + + setURLTemplate: function(urlTemplate) { + return this.set({ + urlTemplate: urlTemplate + }); + }, + + setSubdomains: function(subdomains) { + return this.set({ + subdomains: subdomains + }); + }, + + setTms: function(tms) { + return this.set({ + tms: tms + }); + }, + + getURLTemplate: function(urlTemplate) { + return this.get('urlTemplate'); + }, + + getSubdomains: function(subdomains) { + return this.get('subdomains'); + }, + + getTms: function(tms) { + return this.get('tms'); + } +}); + + + +function MapBase(options) { + var self = this; + + this.options = _.defaults(options, {}); + + this.windshaftMap = null; + this.silent = false; + this._refreshTimer = -1; + + // build template url + if (!this.options.maps_api_template) { + this._buildMapsApiTemplate(this.options); + } +} + +MapBase.BASE_URL = '/api/v1/map'; +MapBase.EMPTY_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + +MapBase.prototype = { + + _buildMapsApiTemplate: function(opts) { + var tilerProtocol = opts.tiler_protocol; + var tilerDomain = opts.tiler_domain; + var tilerPort = (opts.tiler_port != "") ? (":" + opts.tiler_port) : ""; + var username = opts.user_name ? "{user}." : ""; + opts.maps_api_template = [tilerProtocol, "://", username, tilerDomain, tilerPort].join(''); + }, + + _callbackName: function() { + return cdb.core.util.uniqueCallbackName(JSON.stringify(this.toJSON())); + }, + + invalidate: function() { + this.windshaftMap = null; + this.onLayerDefinitionUpdated(); + }, + + onLayerDefinitionUpdated: function() {}, + + setSilent: function(b) { + this.silent = b; + }, + + _definitionUpdated: function() { + if(this.silent) return; + this.invalidate(); + }, + + // Methods to operate with layers + getLayer: function(index) { + return _.clone(this.layers[index]); + }, + + getLayerCount: function() { + return this.layers ? this.layers.length: 0; + }, + + // given number inside layergroup + // returns the real index in tiler layergroup` + getLayerIndexByNumber: function(number) { + var layers = {} + var c = 0; + for(var i = 0; i < this.layers.length; ++i) { + var layer = this.layers[i]; + layers[i] = c; + if(layer.options && !layer.options.hidden) { + ++c; + } + } + return layers[number]; + }, + + /** + * return the layer number by index taking into + * account the hidden layers. + */ + getLayerNumberByIndex: function(index) { + var layers = []; + for(var i = 0; i < this.layers.length; ++i) { + var layer = this.layers[i]; + if(this._isLayerVisible(layer)) { + layers.push(i); + } + } + if (index >= layers.length) { + return -1; + } + return +layers[index]; + }, + + visibleLayers: function() { + var layers = []; + for(var i = 0; i < this.layers.length; ++i) { + var layer = this.layers[i]; + if (this._isLayerVisible(layer)) { + layers.push(layer); + } + } + return layers; + }, + + _isLayerVisible: function(layer) { + if (layer.options && 'hidden' in layer.options) { + return !layer.options.hidden; + } + + return layer.visible !== false; + }, + + setLayer: function(layer, def) { + if(layer < this.getLayerCount() && layer >= 0) { + if (def.options.hidden) { + var i = this.interactionEnabled[layer]; + if (i) { + def.interaction = true + this.setInteraction(layer, false); + } + } else { + if (this.layers[layer].interaction) { + this.setInteraction(layer, true); + delete this.layers[layer].interaction; + } + } + this.layers[layer] = _.clone(def); + } + this.invalidate(); + return this; + }, + + getTooltipData: function(layer) { + var tooltip = this.layers[layer].tooltip; + if (tooltip && tooltip.fields && tooltip.fields.length) { + return tooltip; + } + return null; + }, + + getInfowindowData: function(layer) { + var lyr; + var infowindow = this.layers[layer].infowindow; + if (!infowindow && this.options.layer_definition && (lyr = this.options.layer_definition.layers[layer])) { + infowindow = lyr.infowindow; + } + if (infowindow && infowindow.fields && infowindow.fields.length > 0) { + return infowindow; + } + return null; + }, + + containInfowindow: function() { + var layers = this.options.layer_definition.layers; + for(var i = 0; i < layers.length; ++i) { + var infowindow = layers[i].infowindow; + if (infowindow && infowindow.fields && infowindow.fields.length > 0) { + return true; + } + } + return false; + }, + + containTooltip: function() { + var layers = this.options.layer_definition.layers; + for(var i = 0; i < layers.length; ++i) { + var tooltip = layers[i].tooltip; + if (tooltip && tooltip.fields && tooltip.fields.length) { + return true; + } + } + return false; + }, + + getSubLayer: function(index) { + var layer = this.layers[index]; + layer.sub = layer.sub || SubLayerFactory.createSublayer(layer.type, this, index); + return layer.sub; + }, + + getSubLayerCount: function() { + return this.getLayerCount(); + }, + + getSubLayers: function() { + var layers = [] + for (var i = 0; i < this.getSubLayerCount(); ++i) { + layers.push(this.getSubLayer(i)) + } + return layers; + } +}; + +// TODO: This is actually an AnonymousMap -> Rename? +function LayerDefinition(layerDefinition, options, widgets) { + MapBase.call(this, options); + this.endPoint = MapBase.BASE_URL; + this.setLayerDefinition(layerDefinition, { silent: true }); + this.widgets = widgets; +} + +/** + * Generates the MapConfig definition for a list of sublayers. + * + * ``sublayers`` should be an array, an exception is thrown otherwise. + * + */ +LayerDefinition.layerDefFromSubLayers = function(sublayers) { + + if(!sublayers || sublayers.length === undefined) throw new Error("sublayers should be an array"); + + sublayers = _.map(sublayers, function(sublayer) { + var type = sublayer.type; + delete sublayer.type; + return { + type: type, + options: sublayer + } + }); + + var layerDefinition = { + version: '1.3.0', + stat_tag: 'API', + layers: sublayers + } + + return new LayerDefinition(layerDefinition, {}).toJSON(); +}; + +LayerDefinition.prototype = _.extend({}, MapBase.prototype, { + + setLayerDefinition: function(layerDefinition, options) { + options = options || {}; + this.version = layerDefinition.version || '1.0.0'; + this.stat_tag = layerDefinition.stat_tag; + this.layers = _.clone(layerDefinition.layers); + if(!options.silent) { + this._definitionUpdated(); + } + }, + + toJSON: function() { + var obj = {}; + obj.version = this.version; + if(this.stat_tag) { + obj.stat_tag = this.stat_tag; + } + obj.layers = []; + var layers = this.visibleLayers(); + for(var i = 0; i < layers.length; ++i) { + var sublayer = this.getSubLayer(this.getLayerNumberByIndex(i)); + obj.layers.push(sublayer.toJSON()); + } + + // Widgets + if (this.widgets && this.widgets.length) { + obj.lists = {}; + + var lists = _.filter(this.widgets, function(widget){ + return widget.type === 'list' + }); + + lists.forEach(function(list) { + obj.lists[list.options.id] = { + "sql": list.options.sql, + "columns": list.options.columns + } + }) + } + + return obj; + }, + + removeLayer: function(layer) { + if(layer < this.getLayerCount() && layer >= 0) { + this.layers.splice(layer, 1); + this.interactionEnabled.splice(layer, 1); + this._reorderSubLayers(); + this.invalidate(); + } + return this; + }, + + _reorderSubLayers: function() { + for(var i = 0; i < this.layers.length; ++i) { + var layer = this.layers[i]; + if(layer.sub) { + layer.sub._setPosition(i); + } + } + }, + + addLayer: function(def, index) { + index = index === undefined ? this.getLayerCount(): index; + if(index <= this.getLayerCount() && index >= 0) { + + var type = def.type || 'cartodb'; + delete def.type; + + this.layers.splice(index, 0, { + type: type, + options: def + }); + + var sublayer = this.getSubLayer(index); + if (sublayer.isValid()) { + this._definitionUpdated(); + } else { // Remove it from the definition + sublayer.remove(); + throw 'Layer definition should contain all the required attributes'; + } + } + return this; + }, + + /** + * set interactivity attributes for a layer. + * if attributes are passed as first param layer 0 is + * set + */ + setInteractivity: function(layer, attributes) { + if(attributes === undefined) { + attributes = layer; + layer = 0; + } + + if(layer >= this.getLayerCount() && layer < 0) { + throw new Error("layer does not exist"); + } + + if(typeof(attributes) == 'string') { + attributes = attributes.split(','); + } + + for(var i = 0; i < attributes.length; ++i) { + attributes[i] = attributes[i].replace(/ /g, ''); + } + + this.layers[layer].options.interactivity = attributes; + this._definitionUpdated(); + return this; + }, + + setQuery: function(layer, sql) { + if(sql === undefined) { + sql = layer; + layer = 0; + } + this.layers[layer].options.sql = sql + this._definitionUpdated(); + }, + + getQuery: function(layer) { + layer = layer || 0; + return this.layers[layer].options.sql + }, + + /** + * Change style of the tiles + * @params {style} New carto for the tiles + */ + setCartoCSS: function(layer, style, version) { + if(version === undefined) { + version = style; + style = layer; + layer = 0; + } + + version = version || cartodb.CARTOCSS_DEFAULT_VERSION; + + this.layers[layer].options.cartocss = style; + this.layers[layer].options.cartocss_version = version; + this._definitionUpdated(); + }, + + /** + * adds a new sublayer to the layer with the sql and cartocss params + */ + createSubLayer: function(attrs, options) { + this.addLayer(attrs); + return this.getSubLayer(this.getLayerCount() - 1); + } +}); + +function NamedMap(named_map, options, widgets) { + MapBase.call(this, options); + this.options.pngParams.push('auth_token'); + this.options.gridParams.push('auth_token'); + this.setLayerDefinition(named_map, options); + this.stat_tag = named_map.stat_tag; + this.widgets = widgets; +} + +NamedMap.prototype = _.extend({}, MapBase.prototype, { + + getSubLayer: function(index) { + var layer = this.layers[index]; + // for named maps we don't know how many layers are defined so + // we create the layer on the fly + if (!layer) { + layer = this.layers[index] = { + options: {} + }; + } + layer.sub = layer.sub || SubLayerFactory.createSublayer(layer.type, this, index); + return layer.sub; + }, + + setLayerDefinition: function(named_map, options) { + options = options || {} + this.endPoint = MapBase.BASE_URL + '/named/' + named_map.name; + this.JSONPendPoint = MapBase.BASE_URL + '/named/' + named_map.name + '/jsonp'; + this.layers = _.clone(named_map.layers) || []; + for(var i = 0; i < this.layers.length; ++i) { + var layer = this.layers[i]; + layer.options = layer.options || { 'hidden': layer.visible === false }; + layer.options.layer_name = layer.layer_name; + } + this.named_map = named_map; + var token = named_map.auth_token || options.auth_token; + if (token) { + this.setAuthToken(token); + } + if(!options.silent) { + this.invalidate(); + } + }, + + setAuthToken: function(token) { + // if(!this.isHttps()) { + // throw new Error("https must be used when map has token authentication"); + // } + this.options.extra_params = this.options.extra_params || {}; + this.options.extra_params.auth_token = token; + this.invalidate(); + return this; + }, + + setParams: function(attr, v) { + var params; + if (arguments.length === 2) { + params = {} + params[attr] = v; + } else { + params = attr; + } + if (!this.named_map.params) { + this.named_map.params = {}; + } + for (var k in params) { + if (params[k] === undefined || params[k] === null) { + delete this.named_map.params[k]; + } else { + this.named_map.params[k] = params[k]; + } + } + this.invalidate(); + return this; + }, + + toJSON: function() { + var payload = this.named_map.params || {}; + for(var i = 0; i < this.layers.length; ++i) { + var layer = this.layers[i]; + payload['layer' + i] = this._isLayerVisible(layer) ? 1 : 0; + } + return payload; + }, + + containInfowindow: function() { + var layers = this.layers || []; + for(var i = 0; i < layers.length; ++i) { + var infowindow = layers[i].infowindow; + if (infowindow && infowindow.fields && infowindow.fields.length > 0) { + return true; + } + } + return false; + }, + + containTooltip: function() { + var layers = this.layers || []; + for(var i = 0; i < layers.length; ++i) { + var tooltip = layers[i].tooltip; + if (tooltip) { + return true; + } + } + return false; + }, + + setSQL: function(sql) { + throw new Error("SQL is read-only in NamedMaps"); + }, + + setCartoCSS: function(sql) { + throw new Error("cartocss is read-only in NamedMaps"); + }, + + getCartoCSS: function() { + throw new Error("cartocss can't be accessed in NamedMaps"); + }, + + getSQL: function() { + throw new Error("SQL can't be accessed in NamedMaps"); + }, + + setLayer: function(layer, def) { + var not_allowed_attrs = {'sql': 1, 'cartocss': 1, 'interactivity': 1 }; + + for(var k in def.options) { + if (k in not_allowed_attrs) { + delete def.options[k]; + throw new Error( k + " is read-only in NamedMaps"); + } + } + return MapBase.prototype.setLayer.call(this, layer, def); + }, + + removeLayer: function(layer) { + throw new Error("sublayers are read-only in Named Maps"); + }, + + createSubLayer: function(attrs, options) { + throw new Error("sublayers are read-only in Named Maps"); + }, + + addLayer: function(def, layer) { + throw new Error("sublayers are read-only in Named Maps"); + }, + + // for named maps the layers are always the same (i.e they are + // not removed to hide) so the number does not change + getLayerIndexByNumber: function(number) { + return +number; + } +}); + + + + +/* + * common functions for cartodb connector + */ + +function CartoDBLayerCommon() { + this.visible = true; + this.interactionEnabled = []; +} + +CartoDBLayerCommon.prototype = { + + // the way to show/hidelayer is to set opacity + // removing the interactivty at the same time + show: function() { + this.setOpacity(this.options.previous_opacity === undefined ? 0.99: this.options.previous_opacity); + delete this.options.previous_opacity; + this._interactionDisabled = false; + this.visible = true; + }, + + hide: function() { + if(this.options.previous_opacity == undefined) { + this.options.previous_opacity = this.options.opacity; + } + this.setOpacity(0); + // disable here interaction for all the layers + this._interactionDisabled = true; + this.visible = false; + }, + + toggle: function() { + this.isVisible() ? this.hide() : this.show(); + return this.isVisible(); + }, + + /** + * Returns if the layer is visible or not + */ + isVisible: function() { + return this.visible; + }, + + /** + * Active or desactive interaction + * @params enable {Number} layer number + * @params layer {Boolean} Choose if wants interaction or not + */ + setInteraction: function(layer, b) { + // shift arguments to maintain compatibility + if (b == undefined) { + b = layer; + layer = 0; + } + var layerInteraction; + this.interactionEnabled[layer] = b; + if (!b) { + layerInteraction = this.interaction[layer]; + if(layerInteraction) { + layerInteraction.remove(); + this.interaction[layer] = null; + } + } else { + // if urls is null it means that setInteraction will be called + // when the layergroup token was recieved, then the real interaction + // layer will be created + if (this.model.get('urls')) { + // generate the tilejson from the urls. wax needs it + // var layer_index = this.getLayerIndexByNumber(+layer); + var layer_index = +layer; + var tilejson = this.model.getTileJSONFromTiles(layer_index); + + // remove previous + layerInteraction = this.interaction[layer]; + if(layerInteraction) { + layerInteraction.remove(); + } + var self = this; + + // add the new one + this.interaction[layer] = this.interactionClass() + .map(this.options.map) + .tilejson(tilejson) + .on('on', function(o) { + if (self._interactionDisabled) return; + o.layer = +layer; + self._manageOnEvents(self.options.map, o); + }) + .on('off', function(o) { + if (self._interactionDisabled) return; + o = o || {} + o.layer = +layer; + self._manageOffEvents(self.options.map, o); + }); + } + } + return this; + }, + + setOptions: function (opts) { + + if (typeof opts != "object" || opts.length) { + throw new Error(opts + ' options must be an object'); + } + + _.extend(this.options, opts); + + var opts = this.options; + + this.options.query = this.options.query || "select * from " + this.options.table_name; + if(this.options.query_wrapper) { + this.options.query = _.template(this.options.query_wrapper)({ sql: this.options.query }); + } + + this.setSilent(true); + opts.interaction && this.setInteraction(opts.interaction); + opts.opacity && this.setOpacity(opts.opacity); + opts.query && this.setQuery(opts.query.replace(/\{\{table_name\}\}/g, this.options.table_name)); + opts.tile_style && this.setCartoCSS(opts.tile_style.replace(new RegExp( opts.table_name, "g"), "layer0")); + opts.cartocss && this.setCartoCSS(opts.cartocss); + opts.interactivity && this.setInteractivity(opts.interactivity); + opts.visible ? this.show() : this.hide(); + this.setSilent(false); + this._definitionUpdated(); + }, + + _getLayerDefinition: function() { + // set params + var params = {}; + var opts = this.options; + var sql, cartocss, cartocss_version; + sql = opts.query || "select * from " + opts.table_name; + + if(opts.query_wrapper) { + sql = _.template(opts.query_wrapper)({ sql: sql }); + } + + cartocss = opts.tile_style; + cartocss_version = opts.cartocss_version || '2.1.0'; + + // extra_params? + for (var _param in opts.extra_params) { + var v = opts.extra_params[_param] + params[_param] = v.replace ? v.replace(/\{\{table_name\}\}/g, opts.table_name): v; + } + sql = sql.replace(/\{\{table_name\}\}/g, opts.table_name); + cartocss = cartocss.replace(/\{\{table_name\}\}/g, opts.table_name); + cartocss = cartocss.replace(new RegExp( opts.table_name, "g"), "layer0"); + + return { + sql: sql, + cartocss: cartocss, + cartocss_version: cartocss_version, + params: params, + interactivity: opts.interactivity + } + }, + + error: function(e) { + //console.log(e.error); + }, + + tilesOk: function() { + }, + + _reloadInteraction: function() { + + // Clear existing interaction + this._clearInteraction(); + + // Enable interaction for the layers that have interaction + // (are visible AND have tooltips OR infowindows) + this.model.layers.each(function(layer, index) { + if (layer.hasInteraction()) { + this.setInteraction(index, true); + } + }.bind(this)) + }, + + _clearInteraction: function() { + for(var i in this.interactionEnabled) { + if (this.interactionEnabled.hasOwnProperty(i) && + this.interactionEnabled[i]) { + this.setInteraction(i, false); + } + } + }, + + /** + * Check the tiles + */ + _checkTiles: function() { + var xyz = {z: 4, x: 6, y: 6} + , self = this + , img = new Image() + , urls = this._tileJSON() + + getTiles(function(urls) { + + var grid_url = urls.tiles[0] + .replace(/\{z\}/g,xyz.z) + .replace(/\{x\}/g,xyz.x) + .replace(/\{y\}/g,xyz.y); + + this.options.ajax({ + method: "get", + url: grid_url, + crossDomain: true, + success: function() { + self.tilesOk(); + clearTimeout(timeout) + }, + error: function(xhr, msg, data) { + clearTimeout(timeout); + self.error(xhr.responseText && JSON.parse(xhr.responseText)); + } + }); + }); + + var timeout = setTimeout(function(){ + clearTimeout(timeout); + self.error("tile timeout"); + }, 30000); + + } +}; + +cdb.geo.common = {}; + +cdb.geo.common.CartoDBLogo = { + + /** + * Check if any class already exists + * in the provided container + */ + isWadusAdded: function(container, className) { + // Check if any cartodb-logo exists within container + var a = []; + var re = new RegExp('\\b' + className + '\\b'); + var els = container.getElementsByTagName("*"); + for(var i=0,j=els.length; i 0; + }, + + /** + * Check if browser supports retina images + */ + isRetinaBrowser: function() { + return ('devicePixelRatio' in window && window.devicePixelRatio > 1) || + ('matchMedia' in window && window.matchMedia('(min-resolution:144dpi)') && + window.matchMedia('(min-resolution:144dpi)').matches); + }, + + /** + * Add Cartodb logo + * It needs a position, timeout if it is needed and the container where to add it + */ + addWadus: function(position, timeout, container) { + var self = this; + setTimeout(function() { + if (!self.isWadusAdded(container, 'cartodb-logo')) { + var cartodb_link = document.createElement("div"); + var is_retina = self.isRetinaBrowser(); + cartodb_link.setAttribute('class','cartodb-logo'); + cartodb_link.setAttribute('style',"position:absolute; bottom:0; left:0; display:block; border:none; z-index:1000000;"); + var protocol = location.protocol.indexOf('https') === -1 ? 'http': 'https'; + var link = cdb.config.get('cartodb_logo_link'); + cartodb_link.innerHTML = "CartoDB"; + container.appendChild(cartodb_link); + } + },( timeout || 0 )); + } +}; + + + +(function() { + /** + * base layer for all leaflet layers + */ + var LeafLetLayerView = function(layerModel, leafletLayer, leafletMap) { + this.leafletLayer = leafletLayer; + this.leafletMap = leafletMap; + this.model = layerModel; + + this.setModel(layerModel); + + this.type = layerModel.get('type') || layerModel.get('kind'); + this.type = this.type.toLowerCase(); + }; + + _.extend(LeafLetLayerView.prototype, Backbone.Events); + _.extend(LeafLetLayerView.prototype, { + + setModel: function(model) { + if (this.model) { + this.model.unbind('change', this._modelUpdated, this); + } + this.model = model; + this.model.bind('change', this._modelUpdated, this); + }, + + /** + * remove layer from the map and unbind events + */ + remove: function() { + this.leafletMap.removeLayer(this.leafletLayer); + this.trigger('remove', this); + this.model.unbind(null, null, this); + this.unbind(); + }, + /* + + show: function() { + this.leafletLayer.setOpacity(1.0); + }, + + hide: function() { + this.leafletLayer.setOpacity(0.0); + }, + */ + + /** + * reload the tiles + */ + reload: function() { + this.leafletLayer.redraw(); + } + + }); + + + cdb.geo.LeafLetLayerView = LeafLetLayerView; + + +})(); + + +(function() { + +if(typeof(L) == "undefined") + return; + +/** + * this is a dummy layer class that modifies the leaflet DOM element background + * instead of creating a layer with div + */ +var LeafLetPlainLayerView = L.Class.extend({ + includes: L.Mixin.Events, + + initialize: function(layerModel, leafletMap) { + cdb.geo.LeafLetLayerView.call(this, layerModel, this, leafletMap); + }, + + onAdd: function() { + this.redraw(); + }, + + onRemove: function() { + var div = this.leafletMap.getContainer() + div.style.background = 'none'; + }, + + _modelUpdated: function() { + this.redraw(); + }, + + redraw: function() { + var div = this.leafletMap.getContainer() + div.style.backgroundColor = this.model.get('color') || '#FFF'; + + if (this.model.get('image')) { + var st = 'transparent url(' + this.model.get('image') + ') repeat center center'; + div.style.background = st + } + }, + + // this method + setZIndex: function() { + } + +}); + +_.extend(LeafLetPlainLayerView.prototype, cdb.geo.LeafLetLayerView.prototype); + +cdb.geo.LeafLetPlainLayerView = LeafLetPlainLayerView; + +})(); + + +(function() { + +if(typeof(L) == "undefined") + return; + +var LeafLetTiledLayerView = L.TileLayer.extend({ + initialize: function(layerModel, leafletMap) { + L.TileLayer.prototype.initialize.call(this, layerModel.get('urlTemplate'), { + tms: layerModel.get('tms'), + attribution: layerModel.get('attribution'), + minZoom: layerModel.get('minZoom'), + maxZoom: layerModel.get('maxZoom'), + subdomains: layerModel.get('subdomains') || 'abc', + errorTileUrl: layerModel.get('errorTileUrl'), + opacity: layerModel.get('opacity') + }); + cdb.geo.LeafLetLayerView.call(this, layerModel, this, leafletMap); + } + +}); + +_.extend(LeafLetTiledLayerView.prototype, cdb.geo.LeafLetLayerView.prototype, { + + _modelUpdated: function() { + _.defaults(this.leafletLayer.options, _.clone(this.model.attributes)); + this.leafletLayer.options.subdomains = this.model.get('subdomains') || 'abc'; + this.leafletLayer.options.attribution = this.model.get('attribution'); + this.leafletLayer.options.maxZoom = this.model.get('maxZoom'); + this.leafletLayer.options.minZoom = this.model.get('minZoom'); + // set url and reload + this.leafletLayer.setUrl(this.model.get('urlTemplate')); + } + +}); + +cdb.geo.LeafLetTiledLayerView = LeafLetTiledLayerView; + +})(); + + +(function() { + + if(typeof(L) == "undefined") + return; + + var stamenSubstitute = function stamenSubstitute(type) { + return { + url: 'http://{s}.basemaps.cartocdn.com/'+ type +'_all/{z}/{x}/{y}.png', + subdomains: 'abcd', + minZoom: 0, + maxZoom: 18, + attribution: 'Map designs by Stamen. Data by OpenStreetMap, Provided by CartoDB' + }; + }; + + var nokiaSubstitute = function nokiaSubstitute(type) { + return { + url: 'https://{s}.maps.nlp.nokia.com/maptile/2.1/maptile/newest/'+ type +'.day/{z}/{x}/{y}/256/png8?lg=eng&token=A7tBPacePg9Mj_zghvKt9Q&app_id=KuYppsdXZznpffJsKT24', + subdomains: '1234', + minZoom: 0, + maxZoom: 21, + attribution: '©2012 Nokia Terms of use' + }; + }; + + var substitutes = { + roadmap: nokiaSubstitute('normal'), + gray_roadmap: stamenSubstitute('light'), + dark_roadmap: stamenSubstitute('dark'), + hybrid: nokiaSubstitute('hybrid'), + terrain: nokiaSubstitute('terrain'), + satellite: nokiaSubstitute('satellite') + }; + + var LeafLetGmapsTiledLayerView = L.TileLayer.extend({ + initialize: function(layerModel, leafletMap) { + var substitute = substitutes[layerModel.get('base_type')]; + L.TileLayer.prototype.initialize.call(this, substitute.url, { + tms: false, + attribution: substitute.attribution, + minZoom: substitute.minZoom, + maxZoom: substitute.maxZoom, + subdomains: substitute.subdomains, + errorTileUrl: '', + opacity: 1 + }); + cdb.geo.LeafLetLayerView.call(this, layerModel, this, leafletMap); + } + + }); + + _.extend(LeafLetGmapsTiledLayerView.prototype, cdb.geo.LeafLetLayerView.prototype, { + + _modelUpdated: function() { + // do nothing, this map type does not support updating + } + + }); + + cdb.geo.LeafLetGmapsTiledLayerView = LeafLetGmapsTiledLayerView; + +})(); + + +(function() { + +if(typeof(L) == "undefined") + return; + +var LeafLetWMSLayerView = L.TileLayer.WMS.extend({ + initialize: function(layerModel, leafletMap) { + + L.TileLayer.WMS.prototype.initialize.call(this, layerModel.get('urlTemplate'), { + attribution: layerModel.get('attribution'), + layers: layerModel.get('layers'), + format: layerModel.get('format'), + transparent: layerModel.get('transparent'), + minZoom: layerModel.get('minZomm'), + maxZoom: layerModel.get('maxZoom'), + subdomains: layerModel.get('subdomains') || 'abc', + errorTileUrl: layerModel.get('errorTileUrl'), + opacity: layerModel.get('opacity') + }); + + cdb.geo.LeafLetLayerView.call(this, layerModel, this, leafletMap); + } + +}); + +_.extend(LeafLetWMSLayerView.prototype, cdb.geo.LeafLetLayerView.prototype, { + + _modelUpdated: function() { + _.defaults(this.leafletLayer.options, _.clone(this.model.attributes)); + this.leafletLayer.setUrl(this.model.get('urlTemplate')); + } + +}); + +cdb.geo.LeafLetWMSLayerView = LeafLetWMSLayerView; + +})(); + + +(function() { + +if(typeof(L) == "undefined") + return; + + +L.CartoDBGroupLayerBase = L.TileLayer.extend({ + + interactionClass: wax.leaf.interaction, + + includes: [ + cdb.geo.LeafLetLayerView.prototype, + //LayerDefinition.prototype, + CartoDBLayerCommon.prototype + ], + + options: { + opacity: 0.99, + attribution: cdb.config.get('cartodb_attributions'), + debug: false, + visible: true, + added: false, + tiler_domain: "cartodb.com", + tiler_port: "80", + tiler_protocol: "http", + sql_api_domain: "cartodb.com", + sql_api_port: "80", + sql_api_protocol: "http", + maxZoom: 30, // default leaflet zoom level for a layers is 18, raise it + extra_params: { + }, + cdn_url: null, + subdomains: null + }, + + + initialize: function (options) { + options = options || {}; + // Set options + L.Util.setOptions(this, options); + + this.fire = this.trigger; + + CartoDBLayerCommon.call(this); + L.TileLayer.prototype.initialize.call(this); + this.interaction = []; + this.addProfiling(); + }, + + addProfiling: function() { + this.bind('tileloadstart', function(e) { + var s = this.tileStats || (this.tileStats = {}); + s[e.tile.src] = cartodb.core.Profiler.metric('cartodb-js.tile.png.load.time').start(); + }); + var finish = function(e) { + var s = this.tileStats && this.tileStats[e.tile.src]; + s && s.end(); + }; + this.bind('tileload', finish); + this.bind('tileerror', function(e) { + cartodb.core.Profiler.metric('cartodb-js.tile.png.error').inc(); + finish(e); + }); + }, + + + // overwrite getTileUrl in order to + // support different tiles subdomains in tilejson way + getTileUrl: function (tilePoint) { + var EMPTY_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + this._adjustTilePoint(tilePoint); + + var tiles = [EMPTY_GIF]; + if(this.tilejson) { + tiles = this.tilejson.tiles; + } + + var index = (tilePoint.x + tilePoint.y) % tiles.length; + + return L.Util.template(tiles[index], L.Util.extend({ + z: this._getZoomForUrl(), + x: tilePoint.x, + y: tilePoint.y + }, this.options)); + }, + + /** + * Change opacity of the layer + * @params {Integer} New opacity + */ + setOpacity: function(opacity) { + + if (isNaN(opacity) || opacity>1 || opacity<0) { + throw new Error(opacity + ' is not a valid value'); + } + + // Leaflet only accepts 0-0.99... Weird! + this.options.opacity = Math.min(opacity, 0.99); + + if (this.options.visible) { + L.TileLayer.prototype.setOpacity.call(this, this.options.opacity); + this.fire('updated'); + } + }, + + + /** + * When Leaflet adds the layer... go! + * @params {map} + */ + onAdd: function(map) { + var self = this; + this.options.map = map; + + // Add cartodb logo + if (this.options.cartodb_logo != false) + cdb.geo.common.CartoDBLogo.addWadus({ left:8, bottom:8 }, 0, map._container); + + this.model.bind('change:urls', function() { + self.__update(function() { + // if while the layer was processed in the server is removed + // it should not be added to the map + var id = L.stamp(self); + if (!map._layers[id]) { + return; + } + + L.TileLayer.prototype.onAdd.call(self, map); + self.fire('added'); + self.options.added = true; + }); + }); + }, + + + /** + * When removes the layer, destroy interactivity if exist + */ + onRemove: function(map) { + if(this.options.added) { + this.options.added = false; + L.TileLayer.prototype.onRemove.call(this, map); + } + }, + + /** + * Update CartoDB layer + * generates a new url for tiles and refresh leaflet layer + * do not collide with leaflet _update + */ + __update: function(done) { + var self = this; + this.fire('updated'); + this.fire('loading'); + var map = this.options.map; + + var tilejson = self.model.get('urls'); + if(tilejson) { + self.tilejson = tilejson; + self.setUrl(self.tilejson.tiles[0]); + // manage interaction + self._reloadInteraction(); + self.ok && self.ok(); + done && done(); + } else { + self.error && self.error(err); + done && done(); + } + }, + + + _checkLayer: function() { + if (!this.options.added) { + throw new Error('the layer is not still added to the map'); + } + }, + + /** + * Set a new layer attribution + * @params {String} New attribution string + */ + setAttribution: function(attribution) { + this._checkLayer(); + + // Remove old one + this.map.attributionControl.removeAttribution(this.options.attribution); + + // Set new attribution in the options + this.options.attribution = attribution; + + // Change text + this.map.attributionControl.addAttribution(this.options.attribution); + + // Change in the layer + this.options.attribution = this.options.attribution; + this.tilejson.attribution = this.options.attribution; + + this.fire('updated'); + }, + + /** + * Bind events for wax interaction + * @param {Object} Layer map object + * @param {Event} Wax event + */ + _manageOnEvents: function(map, o) { + var layer_point = this._findPos(map,o); + + if (!layer_point || isNaN(layer_point.x) || isNaN(layer_point.y)) { + // If layer_point doesn't contain x and y, + // we can't calculate event map position + return false; + } + + var latlng = map.layerPointToLatLng(layer_point); + var event_type = o.e.type.toLowerCase(); + var screenPos = map.layerPointToContainerPoint(layer_point); + + switch (event_type) { + case 'mousemove': + if (this.options.featureOver) { + return this.options.featureOver(o.e,latlng, screenPos, o.data, o.layer); + } + break; + + case 'click': + case 'touchend': + case 'touchmove': // for some reason android browser does not send touchend + case 'mspointerup': + case 'pointerup': + case 'pointermove': + if (this.options.featureClick) { + this.options.featureClick(o.e,latlng, screenPos, o.data, o.layer); + } + break; + default: + break; + } + }, + + + /** + * Bind off event for wax interaction + */ + _manageOffEvents: function(map, o) { + if (this.options.featureOut) { + return this.options.featureOut && this.options.featureOut(o.e, o.layer); + } + }, + + /** + * Get the Leaflet Point of the event + * @params {Object} Map object + * @params {Object} Wax event object + */ + _findPos: function (map, o) { + var curleft = 0; + var curtop = 0; + var obj = map.getContainer(); + + var x, y; + if (o.e.changedTouches && o.e.changedTouches.length > 0) { + x = o.e.changedTouches[0].clientX + window.scrollX; + y = o.e.changedTouches[0].clientY + window.scrollY; + } else { + x = o.e.clientX; + y = o.e.clientY; + } + + // If the map is fixed at the top of the window, we can't use offsetParent + // cause there might be some scrolling that we need to take into account. + if (obj.offsetParent && obj.offsetTop > 0) { + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + var point = this._newPoint( + x - curleft, y - curtop); + } else { + var rect = obj.getBoundingClientRect(); + var scrollX = (window.scrollX || window.pageXOffset); + var scrollY = (window.scrollY || window.pageYOffset); + var point = this._newPoint( + (o.e.clientX? o.e.clientX: x) - rect.left - obj.clientLeft - scrollX, + (o.e.clientY? o.e.clientY: y) - rect.top - obj.clientTop - scrollY); + } + return map.containerPointToLayerPoint(point); + }, + + /** + * Creates an instance of a Leaflet Point + */ + _newPoint: function(x, y) { + return new L.Point(x, y); + } +}); + +L.CartoDBGroupLayer = L.CartoDBGroupLayerBase.extend({ + _modelUpdated: function() {} +}); + +function layerView(base) { + var layerViewClass = base.extend({ + + includes: [ + cdb.geo.LeafLetLayerView.prototype, + Backbone.Events + ], + + initialize: function(layerModel, leafletMap) { + var self = this; + var hovers = []; + + var opts = _.clone(layerModel.attributes); + + opts.map = leafletMap; + + var // preserve the user's callbacks + _featureOver = opts.featureOver, + _featureOut = opts.featureOut, + _featureClick = opts.featureClick; + + var previousEvent; + var eventTimeout = -1; + + opts.featureOver = function(e, latlon, pxPos, data, layer) { + if (!hovers[layer]) { + self.trigger('layerenter', e, latlon, pxPos, data, layer); + } + hovers[layer] = 1; + _featureOver && _featureOver.apply(this, arguments); + self.featureOver && self.featureOver.apply(self, arguments); + // if the event is the same than before just cancel the event + // firing because there is a layer on top of it + if (e.timeStamp === previousEvent) { + clearTimeout(eventTimeout); + } + eventTimeout = setTimeout(function() { + self.trigger('mouseover', e, latlon, pxPos, data, layer); + self.trigger('layermouseover', e, latlon, pxPos, data, layer); + }, 0); + previousEvent = e.timeStamp; + + }; + + opts.featureOut = function(m, layer) { + if (hovers[layer]) { + self.trigger('layermouseout', layer); + } + hovers[layer] = 0; + if(!_.any(hovers)) { + self.trigger('mouseout'); + } + _featureOut && _featureOut.apply(this, arguments); + self.featureOut && self.featureOut.apply(self, arguments); + }; + + opts.featureClick = _.debounce(function() { + _featureClick && _featureClick.apply(self, arguments); + self.featureClick && self.featureClick.apply(self, arguments); + }, 10); + + + base.prototype.initialize.call(this, opts); + cdb.geo.LeafLetLayerView.call(this, layerModel, this, leafletMap); + + }, + + featureOver: function(e, latlon, pixelPos, data, layer) { + // dont pass leaflet lat/lon + this.trigger('featureOver', e, [latlon.lat, latlon.lng], pixelPos, data, layer); + }, + + featureOut: function(e, layer) { + this.trigger('featureOut', e, layer); + }, + + featureClick: function(e, latlon, pixelPos, data, layer) { + // dont pass leaflet lat/lon + this.trigger('featureClick', e, [latlon.lat, latlon.lng], pixelPos, data, layer); + }, + + error: function(e) { + this.trigger('error', e ? (e.errors || e) : 'unknown error'); + this.model.trigger('error', e?e.errors:'unknown error'); + }, + + ok: function(e) { + this.model.trigger('tileOk'); + } + }); + + return layerViewClass; +} + +L.NamedMap = L.CartoDBGroupLayerBase.extend({ + includes: [ + cdb.geo.LeafLetLayerView.prototype, + NamedMap.prototype, + CartoDBLayerCommon.prototype + ], + + initialize: function (options) { + options = options || {}; + // Set options + L.Util.setOptions(this, options); + + // Some checks + if (!options.named_map && !options.sublayers) { + throw new Error('cartodb-leaflet needs at least the named_map'); + } + + NamedMap.call(this, this.options.named_map, this.options); + + this.fire = this.trigger; + + CartoDBLayerCommon.call(this); + L.TileLayer.prototype.initialize.call(this); + this.interaction = []; + this.addProfiling(); + }, + + _modelUpdated: function() { + this.setLayerDefinition(this.model.get('named_map')); + } +}); + +cdb.geo.LeafLetCartoDBLayerGroupView = layerView(L.CartoDBGroupLayer); +cdb.geo.LeafLetCartoDBNamedMapView = layerView(L.NamedMap); + +})(); + + +(function() { + +if(typeof(L) == "undefined") + return; + +L.CartoDBLayer = L.CartoDBGroupLayer.extend({ + + options: { + query: "SELECT * FROM {{table_name}}", + opacity: 0.99, + attribution: cdb.config.get('cartodb_attributions'), + debug: false, + visible: true, + added: false, + extra_params: {}, + layer_definition_version: '1.0.0' + }, + + + initialize: function (options) { + L.Util.setOptions(this, options); + + if (!options.table_name || !options.user_name || !options.tile_style) { + throw ('cartodb-leaflet needs at least a CartoDB table name, user_name and tile_style'); + } + + L.CartoDBGroupLayer.prototype.initialize.call(this, { + layer_definition: { + version: this.options.layer_definition_version, + layers: [{ + type: 'cartodb', + options: this._getLayerDefinition(), + infowindow: this.options.infowindow + }] + } + }); + + this.setOptions(this.options); + }, + + setQuery: function(layer, sql) { + if(sql === undefined) { + sql = layer; + layer = 0; + } + sql = sql || 'select * from ' + this.options.table_name; + LayerDefinition.prototype.setQuery.call(this, layer, sql); + }, + + /** + * Returns if the layer is visible or not + */ + isVisible: function() { + return this.visible; + }, + + + /** + * Returns if the layer belongs to the map + */ + isAdded: function() { + return this.options.added; + } + +}); + +/** + * leatlet cartodb layer + */ + +var LeafLetLayerCartoDBView = L.CartoDBLayer.extend({ + //var LeafLetLayerCartoDBView = function(layerModel, leafletMap) { + initialize: function(layerModel, leafletMap) { + var self = this; + + _.bindAll(this, 'featureOut', 'featureOver', 'featureClick'); + + var opts = _.clone(layerModel.attributes); + + opts.map = leafletMap; + + var // preserve the user's callbacks + _featureOver = opts.featureOver, + _featureOut = opts.featureOut, + _featureClick = opts.featureClick; + + opts.featureOver = function() { + _featureOver && _featureOver.apply(this, arguments); + self.featureOver && self.featureOver.apply(this, arguments); + }; + + opts.featureOut = function() { + _featureOut && _featureOut.apply(this, arguments); + self.featureOut && self.featureOut.apply(this, arguments); + }; + + opts.featureClick = function() { + _featureClick && _featureClick.apply(this, arguments); + self.featureClick && self.featureClick.apply(opts, arguments); + }; + + layerModel.bind('change:visible', function() { + self.model.get('visible') ? self.show(): self.hide(); + }, this); + + L.CartoDBLayer.prototype.initialize.call(this, opts); + cdb.geo.LeafLetLayerView.call(this, layerModel, this, leafletMap); + + }, + + _modelUpdated: function() { + var attrs = _.clone(this.model.attributes); + this.leafletLayer.setOptions(attrs); + }, + + featureOver: function(e, latlon, pixelPos, data) { + // dont pass leaflet lat/lon + this.trigger('featureOver', e, [latlon.lat, latlon.lng], pixelPos, data, 0); + }, + + featureOut: function(e) { + this.trigger('featureOut', e, 0); + }, + + featureClick: function(e, latlon, pixelPos, data) { + // dont pass leaflet lat/lon + this.trigger('featureClick', e, [latlon.lat, latlon.lng], pixelPos, data, 0); + }, + + reload: function() { + this.model.invalidate(); + //this.redraw(); + }, + + error: function(e) { + this.trigger('error', e?e.error:'unknown error'); + this.model.trigger('tileError', e?e.error:'unknown error'); + }, + + tilesOk: function(e) { + this.model.trigger('tileOk'); + }, + + includes: [ + cdb.geo.LeafLetLayerView.prototype, + Backbone.Events + ] + +}); + +/*_.extend(L.CartoDBLayer.prototype, CartoDBLayerCommon.prototype); + +_.extend( + LeafLetLayerCartoDBView.prototype, + cdb.geo.LeafLetLayerView.prototype, + L.CartoDBLayer.prototype, + Backbone.Events, // be sure this is here to not use the on/off from leaflet + + */ +cdb.geo.LeafLetLayerCartoDBView = LeafLetLayerCartoDBView; + +})(); + +(function() { + +/** + * this module implements all the features related to overlay geometries + * in leaflet: markers, polygons, lines and so on + */ + + +/** + * view for markers + */ +function PointView(geometryModel) { + var self = this; + // events to link + var events = [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'dragstart', + 'drag', + 'dragend' + ]; + + this._eventHandlers = {}; + this.model = geometryModel; + this.points = []; + + var icon = { + iconUrl: this.model.get('iconUrl') || cdb.config.get('assets_url') + '/images/layout/default_marker.png', + iconAnchor: this.model.get('iconAnchor') || [11, 11] + }; + + this.geom = L.GeoJSON.geometryToLayer(geometryModel.get('geojson'), function(geojson, latLng) { + //TODO: create marker depending on the visualizacion options + var p = L.marker(latLng, { + icon: L.icon(icon) + }); + + var i; + for(i = 0; i < events.length; ++i) { + var e = events[i]; + p.on(e, self._eventHandler(e)); + } + return p; + }); + + this.bind('dragend', function(e, pos) { + geometryModel.set({ + geojson: { + type: 'Point', + //geojson is lng,lat + coordinates: [pos[1], pos[0]] + } + }); + }); +} + +PointView.prototype = new GeometryView(); + +PointView.prototype.edit = function() { + this.geom.dragging.enable(); +}; + +/** + * returns a function to handle events fot evtType + */ +PointView.prototype._eventHandler = function(evtType) { + var self = this; + var h = this._eventHandlers[evtType]; + if(!h) { + h = function(e) { + var latlng = e.target.getLatLng(); + var s = [latlng.lat, latlng.lng]; + self.trigger(evtType, e.originalEvent, s); + }; + this._eventHandlers[evtType] = h; + } + return h; +}; + +/** + * view for other geometries (polygons/lines) + */ +function PathView(geometryModel) { + var self = this; + // events to link + var events = [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + ]; + + this._eventHandlers = {}; + this.model = geometryModel; + this.points = []; + + + this.geom = L.GeoJSON.geometryToLayer(geometryModel.get('geojson')); + this.geom.setStyle(geometryModel.get('style')); + + + /*for(var i = 0; i < events.length; ++i) { + var e = events[i]; + this.geom.on(e, self._eventHandler(e)); + }*/ + +} + +PathView.prototype = new GeometryView(); + +PathView.prototype._leafletLayers = function() { + // check if this is a multi-feature or single-feature + if (this.geom.getLayers) { + return this.geom.getLayers(); + } + return [this.geom]; +}; + + +PathView.prototype.enableEdit = function() { + var self = this; + var layers = this._leafletLayers(); + _.each(layers, function(g) { + g.setStyle(self.model.get('style')); + g.on('edit', function() { + self.model.set('geojson', self.geom.toGeoJSON().geometry); + }, self); + }); +}; + +PathView.prototype.disableEdit = function() { + var self = this; + var layers = this._leafletLayers(); + _.each(layers, function(g) { + g.off('edit', null, self); + }); +}; + +PathView.prototype.edit = function(enable) { + var self = this; + var fn = enable ? 'enable': 'disable'; + var layers = this._leafletLayers(); + _.each(layers, function(g) { + g.editing[fn](); + enable ? self.enableEdit(): self.disableEdit(); + }); +}; + +cdb.geo.leaflet = cdb.geo.leaflet || {}; + +cdb.geo.leaflet.PointView = PointView; +cdb.geo.leaflet.PathView = PathView; + + +})(); + +/** +* leaflet implementation of a map +*/ +(function() { + + if(typeof(L) == "undefined") + return; + + /** + * leatlef impl + */ + cdb.geo.LeafletMapView = cdb.geo.MapView.extend({ + + + initialize: function() { + + _.bindAll(this, '_addLayer', '_removeLayer', '_setZoom', '_setCenter', '_setView'); + + cdb.geo.MapView.prototype.initialize.call(this); + + var self = this; + + var center = this.map.get('center'); + + var mapConfig = { + zoomControl: false, + center: new L.LatLng(center[0], center[1]), + zoom: this.map.get('zoom'), + minZoom: this.map.get('minZoom'), + maxZoom: this.map.get('maxZoom') + }; + + + if (this.map.get('bounding_box_ne')) { + //mapConfig.maxBounds = [this.map.get('bounding_box_ne'), this.map.get('bounding_box_sw')]; + } + + if (!this.options.map_object) { + + this.map_leaflet = new L.Map(this.el, mapConfig); + + // remove the "powered by leaflet" + this.map_leaflet.attributionControl.setPrefix(''); + + if (this.map.get("scrollwheel") == false) this.map_leaflet.scrollWheelZoom.disable(); + if (this.map.get("keyboard") == false) this.map_leaflet.keyboard.disable(); + + } else { + + this.map_leaflet = this.options.map_object; + this.setElement(this.map_leaflet.getContainer()); + + var c = self.map_leaflet.getCenter(); + + self._setModelProperty({ center: [c.lat, c.lng] }); + self._setModelProperty({ zoom: self.map_leaflet.getZoom() }); + + // unset bounds to not change mapbounds + self.map.unset('view_bounds_sw', { silent: true }); + self.map.unset('view_bounds_ne', { silent: true }); + } + + this.map.bind('set_view', this._setView, this); + this.map.layers.bind('add', this._addLayer, this); + this.map.layers.bind('remove', this._removeLayer, this); + this.map.layers.bind('reset', this._addLayers, this); + this.map.layers.bind('change:type', this._swicthLayerView, this); + + this.map.geometries.bind('add', this._addGeometry, this); + this.map.geometries.bind('remove', this._removeGeometry, this); + + this._bindModel(); + this._addLayers(); + this.setAttribution(); + + this.map_leaflet.on('layeradd', function(lyr) { + this.trigger('layeradd', lyr, self); + }, this); + + this.map_leaflet.on('zoomstart', function() { + self.trigger('zoomstart'); + }); + + this.map_leaflet.on('click', function(e) { + self.trigger('click', e.originalEvent, [e.latlng.lat, e.latlng.lng]); + }); + + this.map_leaflet.on('dblclick', function(e) { + self.trigger('dblclick', e.originalEvent); + }); + + this.map_leaflet.on('zoomend', function() { + self._setModelProperty({ + zoom: self.map_leaflet.getZoom() + }); + self.trigger('zoomend'); + }, this); + + this.map_leaflet.on('move', function() { + var c = self.map_leaflet.getCenter(); + self._setModelProperty({ center: [c.lat, c.lng] }); + }); + + this.map_leaflet.on('dragend', function() { + var c = self.map_leaflet.getCenter(); + this.trigger('dragend', [c.lat, c.lng]); + }, this); + + this.map_leaflet.on('drag', function() { + var c = self.map_leaflet.getCenter(); + self._setModelProperty({ + center: [c.lat, c.lng] + }); + self.trigger('drag'); + }, this); + + this.map.bind('change:maxZoom', function() { + L.Util.setOptions(self.map_leaflet, { maxZoom: self.map.get('maxZoom') }); + }, this); + + this.map.bind('change:minZoom', function() { + L.Util.setOptions(self.map_leaflet, { minZoom: self.map.get('minZoom') }); + }, this); + + this.trigger('ready'); + + // looks like leaflet dont like to change the bounds just after the inicialization + var bounds = this.map.getViewBounds(); + + if (bounds) { + this.showBounds(bounds); + } + }, + + // this replaces the default functionality to search for + // already added views so they are not replaced + _addLayers: function() { + var self = this; + + var oldLayers = this.layers; + this.layers = {}; + + function findLayerView(layer) { + var lv = _.find(oldLayers, function(layer_view) { + var m = layer_view.model; + return m.isEqual(layer); + }); + return lv; + } + + function canReused(layer) { + return self.map.layers.find(function(m) { + return m.isEqual(layer); + }); + } + + // remove all + for(var layer in oldLayers) { + var layer_view = oldLayers[layer]; + if (!canReused(layer_view.model)) { + layer_view.remove(); + } + } + + this.map.layers.each(function(lyr) { + var lv = findLayerView(lyr); + if (!lv) { + self._addLayer(lyr); + } else { + lv.setModel(lyr); + self.layers[lyr.cid] = lv; + self.trigger('newLayerView', lv, lv.model, self); + } + }); + + }, + + // LAYER VIEWS ARE CREATED HERE + _addLayer: function(layer, layers, opts) { + var self = this; + var lyr, layer_view; + layer_view = cdb.geo.LeafletMapView.createLayer(layer, this.map_leaflet); + if (!layer_view) { + return; + } + return this._addLayerToMap(layer_view, opts); + }, + + + + clean: function() { + //see https://github.com/CloudMade/Leaflet/issues/1101 + L.DomEvent.off(window, 'resize', this.map_leaflet._onResize, this.map_leaflet); + + // remove layer views + for(var layer in this.layers) { + var layer_view = this.layers[layer]; + layer_view.remove(); + delete this.layers[layer]; + } + + // do not change by elder + cdb.core.View.prototype.clean.call(this); + }, + + _setKeyboard: function(model, z) { + if (z) { + this.map_leaflet.keyboard.enable(); + } else { + this.map_leaflet.keyboard.disable(); + } + }, + + _setScrollWheel: function(model, z) { + if (z) { + this.map_leaflet.scrollWheelZoom.enable(); + } else { + this.map_leaflet.scrollWheelZoom.disable(); + } + }, + + _setZoom: function(model, z) { + this._setView(); + }, + + _setCenter: function(model, center) { + this._setView(); + }, + + _setView: function() { + this.map_leaflet.setView(this.map.get("center"), this.map.get("zoom") || 0 ); + }, + + _addGeomToMap: function(geom) { + var geo = cdb.geo.LeafletMapView.createGeometry(geom); + geo.geom.addTo(this.map_leaflet); + return geo; + }, + + _removeGeomFromMap: function(geo) { + this.map_leaflet.removeLayer(geo.geom); + }, + + createLayer: function(layer) { + return cdb.geo.LeafletMapView.createLayer(layer, this.map_leaflet); + }, + + _addLayerToMap: function(layer_view, opts) { + var layer = layer_view.model; + + this.layers[layer.cid] = layer_view; + cdb.geo.LeafletMapView.addLayerToMap(layer_view, this.map_leaflet); + + // reorder layers + for(var i in this.layers) { + var lv = this.layers[i]; + lv.setZIndex(lv.model.get('order')); + } + + if(opts === undefined || !opts.silent) { + this.trigger('newLayerView', layer_view, layer_view.model, this); + } + return layer_view; + }, + + pixelToLatLon: function(pos) { + var point = this.map_leaflet.containerPointToLatLng([pos[0], pos[1]]); + return point; + }, + + latLonToPixel: function(latlon) { + var point = this.map_leaflet.latLngToLayerPoint(new L.LatLng(latlon[0], latlon[1])); + return this.map_leaflet.layerPointToContainerPoint(point); + }, + + // return the current bounds of the map view + getBounds: function() { + var b = this.map_leaflet.getBounds(); + var sw = b.getSouthWest(); + var ne = b.getNorthEast(); + return [ + [sw.lat, sw.lng], + [ne.lat, ne.lng] + ]; + }, + + setAttribution: function() { + var attributionControl = this._getAttributionControl(); + + // Save the attributions that were in the map the first time a new layer + // is added and the attributions of the map have changed + if (!this._originalAttributions) { + this._originalAttributions = Object.keys(attributionControl._attributions); + } + + // Clear the attributions and re-add the original and custom attributions in + // the order we want + attributionControl._attributions = {}; + var newAttributions = this._originalAttributions.concat(this.map.get('attribution')); + _.each(newAttributions, function(attribution) { + attributionControl.addAttribution(attribution); + }); + }, + + _getAttributionControl: function() { + if (this._attributionControl) { + return this._attributionControl; + } + + this._attributionControl = this.map_leaflet.attributionControl; + if (!this._attributionControl) { + this._attributionControl = L.control.attribution({ prefix: '' }); + this.map_leaflet.addControl(this._attributionControl); + } + + return this._attributionControl; + }, + + getSize: function() { + return this.map_leaflet.getSize(); + }, + + panBy: function(p) { + this.map_leaflet.panBy(new L.Point(p.x, p.y)); + }, + + setCursor: function(cursor) { + $(this.map_leaflet.getContainer()).css('cursor', cursor); + }, + + getNativeMap: function() { + return this.map_leaflet; + }, + + invalidateSize: function() { + // there is a race condition in leaflet. If size is invalidated + // and at the same time the center is set the final center is displaced + // so set pan to false so the map is not moved and then force the map + // to be at the place it should be + this.map_leaflet.invalidateSize({ pan: false })//, animate: false }); + this.map_leaflet.setView(this.map.get("center"), this.map.get("zoom") || 0, { + animate: false + }); + } + + }, { + + layerTypeMap: { + "tiled": cdb.geo.LeafLetTiledLayerView, + "wms": cdb.geo.LeafLetWMSLayerView, + "cartodb": cdb.geo.LeafLetLayerCartoDBView, + "carto": cdb.geo.LeafLetLayerCartoDBView, + "plain": cdb.geo.LeafLetPlainLayerView, + + // Substitutes the GMaps baselayer w/ an equivalent Leaflet tiled layer, since not supporting Gmaps anymore + "gmapsbase": cdb.geo.LeafLetGmapsTiledLayerView, + + "layergroup": cdb.geo.LeafLetCartoDBLayerGroupView, + "namedmap": cdb.geo.LeafLetCartoDBNamedMapView, + "torque": function(layer, map) { + return new cdb.geo.LeafLetTorqueLayer(layer, map); + } + }, + + createLayer: function(layer, map) { + var layer_view = null; + var layerClass = this.layerTypeMap[layer.get('type').toLowerCase()]; + + if (layerClass) { + try { + layer_view = new layerClass(layer, map); + } catch(e) { + cdb.log.error("MAP: error creating '" + layer.get('type') + "' layer -> " + e.message); + } + } else { + cdb.log.error("MAP: " + layer.get('type') + " can't be created"); + } + return layer_view; + }, + + addLayerToMap: function(layer_view, map, pos) { + map.addLayer(layer_view.leafletLayer); + if(pos !== undefined) { + if (layer_view.setZIndex) { + layer_view.setZIndex(pos); + } + } + }, + + /** + * create the view for the geometry model + */ + createGeometry: function(geometryModel) { + if(geometryModel.isPoint()) { + return new cdb.geo.leaflet.PointView(geometryModel); + } + return new cdb.geo.leaflet.PathView(geometryModel); + } + + }); + + // set the image path in order to be able to get leaflet icons + // code adapted from leaflet + L.Icon.Default.imagePath = (function () { + var scripts = document.getElementsByTagName('script'), + leafletRe = /\/?cartodb[\-\._]?([\w\-\._]*)\.js\??/; + + var i, len, src, matches; + + for (i = 0, len = scripts.length; i < len; i++) { + src = scripts[i].src; + matches = src.match(leafletRe); + + if (matches) { + var bits = src.split('/') + delete bits[bits.length - 1]; + return bits.join('/') + 'themes/css/images'; + } + } + }()); + +})(); + + +(function() { + +if(typeof(google) == "undefined" || typeof(google.maps) == "undefined") + return; + +/** +* base layer for all google maps +*/ + +var GMapsLayerView = function(layerModel, gmapsLayer, gmapsMap) { + this.gmapsLayer = gmapsLayer; + this.map = this.gmapsMap = gmapsMap; + this.model = layerModel; + this.model.bind('change', this._update, this); + + this.type = layerModel.get('type') || layerModel.get('kind'); + this.type = this.type.toLowerCase(); +}; + +_.extend(GMapsLayerView.prototype, Backbone.Events); +_.extend(GMapsLayerView.prototype, { + + // hack function to search layer inside google maps layers + _searchLayerIndex: function() { + var self = this; + var index = -1; + this.gmapsMap.overlayMapTypes.forEach( + function(layer, i) { + if (layer == self) { + index = i; + } + } + ); + return index; + }, + + /** + * remove layer from the map and unbind events + */ + remove: function() { + if(!this.isBase) { + var self = this; + var idx = this._searchLayerIndex(); + if(idx >= 0) { + this.gmapsMap.overlayMapTypes.removeAt(idx); + } else if (this.gmapsLayer.setMap){ + this.gmapsLayer.setMap(null); + } + this.model.unbind(null, null, this); + this.unbind(); + } + }, + + refreshView: function() { + var self = this; + //reset to update + if(this.isBase) { + var a = '_baseLayer'; + this.gmapsMap.setMapTypeId(null); + this.gmapsMap.mapTypes.set(a, this.gmapsLayer); + this.gmapsMap.setMapTypeId(a); + } else { + var idx = this._searchLayerIndex(); + if(idx >= 0) { + this.gmapsMap.overlayMapTypes.setAt(idx, this); + } + } + }, + + reload: function() { this.refreshView() ; }, + _update: function() { this.refreshView(); } + + +}); + +cdb.geo.GMapsLayerView = GMapsLayerView; + +})(); + + +(function() { + +if(typeof(google) == "undefined" || typeof(google.maps) == "undefined") + return; + +var GMapsBaseLayerView = function(layerModel, gmapsMap) { + cdb.geo.GMapsLayerView.call(this, layerModel, null, gmapsMap); +}; + +_.extend( + GMapsBaseLayerView.prototype, + cdb.geo.GMapsLayerView.prototype, + { + _update: function() { + var m = this.model; + var types = { + "roadmap": google.maps.MapTypeId.ROADMAP, + "gray_roadmap": google.maps.MapTypeId.ROADMAP, + "dark_roadmap": google.maps.MapTypeId.ROADMAP, + "hybrid": google.maps.MapTypeId.HYBRID, + "satellite": google.maps.MapTypeId.SATELLITE, + "terrain": google.maps.MapTypeId.TERRAIN + }; + + this.gmapsMap.setOptions({ + mapTypeId: types[m.get('base_type')] + }); + + this.gmapsMap.setOptions({ + styles: m.get('style') || DEFAULT_MAP_STYLE + }); + }, + remove: function() { } +}); + + +cdb.geo.GMapsBaseLayerView = GMapsBaseLayerView; + + +})(); + + +(function() { + +if(typeof(google) == "undefined" || typeof(google.maps) == "undefined") + return; + +var GMapsPlainLayerView = function(layerModel, gmapsMap) { + this.color = layerModel.get('color') + cdb.geo.GMapsLayerView.call(this, layerModel, this, gmapsMap); +}; + +_.extend( + GMapsPlainLayerView.prototype, + cdb.geo.GMapsLayerView.prototype, { + + _update: function() { + this.color = this.model.get('color') + this.refreshView(); + }, + + getTile: function(coord, zoom, ownerDocument) { + var div = document.createElement('div'); + div.style.width = this.tileSize.x; + div.style.height = this.tileSize.y; + div['background-color'] = this.color; + return div; + }, + + tileSize: new google.maps.Size(256,256), + maxZoom: 100, + minZoom: 0, + name:"plain layer", + alt: "plain layer" +}); + +cdb.geo.GMapsPlainLayerView = GMapsPlainLayerView; + +})(); + + +(function() { + +if(typeof(google) == "undefined" || typeof(google.maps) == "undefined") + return; + +// TILED LAYER +var GMapsTiledLayerView = function(layerModel, gmapsMap) { + cdb.geo.GMapsLayerView.call(this, layerModel, this, gmapsMap); + this.tileSize = new google.maps.Size(256, 256); + this.opacity = 1.0; + this.isPng = true; + this.maxZoom = 22; + this.minZoom = 0; + this.name= 'cartodb tiled layer'; + google.maps.ImageMapType.call(this, this); +}; + +_.extend( + GMapsTiledLayerView.prototype, + cdb.geo.GMapsLayerView.prototype, + google.maps.ImageMapType.prototype, { + + getTileUrl: function(tile, zoom) { + var y = tile.y; + var tileRange = 1 << zoom; + if (y < 0 || y >= tileRange) { + return null; + } + var x = tile.x; + if (x < 0 || x >= tileRange) { + x = (x % tileRange + tileRange) % tileRange; + } + if(this.model.get('tms')) { + y = tileRange - y - 1; + } + var urlPattern = this.model.get('urlTemplate'); + return urlPattern + .replace("{x}",x) + .replace("{y}",y) + .replace("{z}",zoom); + } +}); + +cdb.geo.GMapsTiledLayerView = GMapsTiledLayerView; + + +})(); + +(function() { +// if google maps is not defined do not load the class +if(typeof(google) == "undefined" || typeof(google.maps) == "undefined") { + return; +} + +// helper to get pixel position from latlon +var Projector = function(map) { this.setMap(map); }; +Projector.prototype = new google.maps.OverlayView(); +Projector.prototype.draw = function() {}; +Projector.prototype.latLngToPixel = function(point) { + var p = this.getProjection(); + if(p) { + return p.fromLatLngToContainerPixel(point); + } + return [0, 0]; +}; +Projector.prototype.pixelToLatLng = function(point) { + var p = this.getProjection(); + if(p) { + return p.fromContainerPixelToLatLng(point); + } + return [0, 0]; + //return this.map.getProjection().fromPointToLatLng(point); +}; + +var default_options = { + opacity: 0.99, + attribution: cdb.config.get('cartodb_attributions'), + debug: false, + visible: true, + added: false, + tiler_domain: "cartodb.com", + tiler_port: "80", + tiler_protocol: "http", + sql_api_domain: "cartodb.com", + sql_api_port: "80", + sql_api_protocol: "http", + extra_params: { + }, + cdn_url: null, + subdomains: null +}; + +var OPACITY_FILTER = "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF,endColorstr=#00FFFFFF)"; + +var CartoDBNamedMap = function(opts) { + + this.options = _.defaults(opts, default_options); + this.tiles = 0; + this.tilejson = null; + this.interaction = []; + + if (!opts.named_map && !opts.sublayers) { + throw new Error('cartodb-gmaps needs at least the named_map'); + } + + // Add CartoDB logo + if (this.options.cartodb_logo != false) + cdb.geo.common.CartoDBLogo.addWadus({ left: 74, bottom:8 }, 2000, this.options.map.getDiv()); + + wax.g.connector.call(this, opts); + + // lovely wax connector overwrites options so set them again + // TODO: remove wax.connector here + _.extend(this.options, opts); + this.projector = new Projector(opts.map); + NamedMap.call(this, this.options.named_map, this.options); + CartoDBLayerCommon.call(this); + // precache + this.update(); +}; + + +var CartoDBLayerGroup = function(opts) { + + this.options = _.defaults(opts, default_options); + this.tiles = 0; + this.tilejson = null; + this.interaction = []; + + if (!opts.layer_definition && !opts.sublayers) { + throw new Error('cartodb-leaflet needs at least the layer_definition or sublayer list'); + } + + // if only sublayers is available, generate layer_definition from it + if(!opts.layer_definition) { + opts.layer_definition = LayerDefinition.layerDefFromSubLayers(opts.sublayers); + } + + // Add CartoDB logo + if (this.options.cartodb_logo != false) + cdb.geo.common.CartoDBLogo.addWadus({ left: 74, bottom:8 }, 2000, this.options.map.getDiv()); + + wax.g.connector.call(this, opts); + + // lovely wax connector overwrites options so set them again + // TODO: remove wax.connector here + _.extend(this.options, opts); + this.projector = new Projector(opts.map); + LayerDefinition.call(this, opts.layer_definition, this.options); + CartoDBLayerCommon.call(this); + // precache + this.update(); +}; + +function setImageOpacityIE8(img, opacity) { + var v = Math.round(opacity*100); + if (v >= 99) { + img.style.filter = OPACITY_FILTER; + } else { + img.style.filter = "alpha(opacity=" + (opacity) + ");"; + } +} + +function CartoDBLayerGroupBase() {} + +CartoDBLayerGroupBase.prototype.setOpacity = function(opacity) { + if (isNaN(opacity) || opacity > 1 || opacity < 0) { + throw new Error(opacity + ' is not a valid value, should be in [0, 1] range'); + } + this.opacity = this.options.opacity = opacity; + for(var key in this.cache) { + var img = this.cache[key]; + img.style.opacity = opacity; + setImageOpacityIE8(img, opacity); + } + +}; + +CartoDBLayerGroupBase.prototype.setAttribution = function() {}; + +CartoDBLayerGroupBase.prototype.getTile = function(coord, zoom, ownerDocument) { + var EMPTY_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + + var self = this; + var ie = 'ActiveXObject' in window, + ielt9 = ie && !document.addEventListener; + + this.options.added = true; + + if(this.tilejson === null) { + var key = zoom + '/' + coord.x + '/' + coord.y; + var i = this.cache[key] = new Image(256, 256); + i.src = EMPTY_GIF; + i.setAttribute('gTileKey', key); + i.style.opacity = this.options.opacity; + return i; + } + + var im = wax.g.connector.prototype.getTile.call(this, coord, zoom, ownerDocument); + + // in IE8 semi transparency does not work and needs filter + if( ielt9 ) { + setImageOpacityIE8(im, this.options.opacity); + } + im.style.opacity = this.options.opacity; + if (this.tiles === 0) { + this.loading && this.loading(); + } + + this.tiles++; + + var loadTime = cartodb.core.Profiler.metric('cartodb-js.tile.png.load.time').start(); + + var finished = function() { + loadTime.end(); + self.tiles--; + if (self.tiles === 0) { + self.finishLoading && self.finishLoading(); + } + }; + im.onload = finished; + im.onerror = function() { + cartodb.core.Profiler.metric('cartodb-js.tile.png.error').inc(); + finished(); + } + + return im; +}; + +CartoDBLayerGroupBase.prototype.onAdd = function () { + //this.update(); +}; + +CartoDBLayerGroupBase.prototype.clear = function () { + this._clearInteraction(); + self.finishLoading && self.finishLoading(); +}; + +CartoDBLayerGroupBase.prototype.update = function (done) { + var self = this; + this.loading && this.loading(); + this.getTiles(function(urls, err) { + if(urls) { + self.tilejson = urls; + self.options.tiles = urls.tiles; + self.tiles = 0; + self.cache = {}; + self._reloadInteraction(); + self.refreshView(); + self.ok && self.ok(); + done && done(); + } else { + self.error && self.error(err) + } + }); +}; + +CartoDBLayerGroupBase.prototype.refreshView = function() { + var self = this; + var map = this.options.map; + map.overlayMapTypes.forEach( + function(layer, i) { + if (layer == self) { + map.overlayMapTypes.setAt(i, self); + return; + } + } + ); +} +CartoDBLayerGroupBase.prototype.onLayerDefinitionUpdated = function() { + this.update(); +} + +CartoDBLayerGroupBase.prototype._checkLayer = function() { + if (!this.options.added) { + throw new Error('the layer is not still added to the map'); + } +} + +CartoDBLayerGroupBase.prototype._findPos = function (map,o) { + var curleft = 0; + var curtop = 0; + var obj = map.getDiv(); + + var x, y; + if (o.e.changedTouches && o.e.changedTouches.length > 0) { + x = o.e.changedTouches[0].clientX + window.scrollX; + y = o.e.changedTouches[0].clientY + window.scrollY; + } else { + x = o.e.clientX; + y = o.e.clientY; + } + + // If the map is fixed at the top of the window, we can't use offsetParent + // cause there might be some scrolling that we need to take into account. + if (obj.offsetParent && obj.offsetTop > 0) { + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + var point = this._newPoint( + x - curleft, y - curtop); + } else { + var rect = obj.getBoundingClientRect(); + var scrollX = (window.scrollX || window.pageXOffset); + var scrollY = (window.scrollY || window.pageYOffset); + var point = this._newPoint( + (o.e.clientX? o.e.clientX: x) - rect.left - obj.clientLeft - scrollX, + (o.e.clientY? o.e.clientY: y) - rect.top - obj.clientTop - scrollY); + } + return point; +}; + +/** + * Creates an instance of a google.maps Point + */ +CartoDBLayerGroupBase.prototype._newPoint = function(x, y) { + return new google.maps.Point(x, y); +}; + +CartoDBLayerGroupBase.prototype._manageOffEvents = function(map, o){ + if (this.options.featureOut) { + return this.options.featureOut && this.options.featureOut(o.e, o.layer); + } +}; + + +CartoDBLayerGroupBase.prototype._manageOnEvents = function(map,o) { + var point = this._findPos(map, o); + var latlng = this.projector.pixelToLatLng(point); + var event_type = o.e.type.toLowerCase(); + + + switch (event_type) { + case 'mousemove': + if (this.options.featureOver) { + return this.options.featureOver(o.e,latlng, point, o.data, o.layer); + } + break; + + case 'click': + case 'touchend': + case 'touchmove': // for some reason android browser does not send touchend + case 'mspointerup': + case 'pointerup': + case 'pointermove': + if (this.options.featureClick) { + this.options.featureClick(o.e,latlng, point, o.data, o.layer); + } + break; + default: + break; + } +} + +// CartoDBLayerGroup type +CartoDBLayerGroup.Projector = Projector; +CartoDBLayerGroup.prototype = new wax.g.connector(); +_.extend(CartoDBLayerGroup.prototype, LayerDefinition.prototype, CartoDBLayerGroupBase.prototype, CartoDBLayerCommon.prototype); +CartoDBLayerGroup.prototype.interactionClass = wax.g.interaction; + + +// CartoDBNamedMap +CartoDBNamedMap.prototype = new wax.g.connector(); +_.extend(CartoDBNamedMap.prototype, NamedMap.prototype, CartoDBLayerGroupBase.prototype, CartoDBLayerCommon.prototype); +CartoDBNamedMap.prototype.interactionClass = wax.g.interaction; + + +// export +cdb.geo.CartoDBLayerGroupGMaps = CartoDBLayerGroup; +cdb.geo.CartoDBNamedMapGMaps = CartoDBNamedMap; + +/* + * + * cartodb layer group view + * + */ + +function LayerGroupView(base) { + var GMapsCartoDBLayerGroupView = function(layerModel, gmapsMap) { + var self = this; + var hovers = []; + + _.bindAll(this, 'featureOut', 'featureOver', 'featureClick'); + + var opts = _.clone(layerModel.attributes); + + opts.map = gmapsMap; + + var // preserve the user's callbacks + _featureOver = opts.featureOver, + _featureOut = opts.featureOut, + _featureClick = opts.featureClick; + + var previousEvent; + var eventTimeout = -1; + + opts.featureOver = function(e, latlon, pxPos, data, layer) { + if (!hovers[layer]) { + self.trigger('layerenter', e, latlon, pxPos, data, layer); + } + hovers[layer] = 1; + _featureOver && _featureOver.apply(this, arguments); + self.featureOver && self.featureOver.apply(this, arguments); + + // if the event is the same than before just cancel the event + // firing because there is a layer on top of it + if (e.timeStamp === previousEvent) { + clearTimeout(eventTimeout); + } + eventTimeout = setTimeout(function() { + self.trigger('mouseover', e, latlon, pxPos, data, layer); + self.trigger('layermouseover', e, latlon, pxPos, data, layer); + }, 0); + previousEvent = e.timeStamp; + }; + + opts.featureOut = function(m, layer) { + if (hovers[layer]) { + self.trigger('layermouseout', layer); + } + hovers[layer] = 0; + if(!_.any(hovers)) { + self.trigger('mouseout'); + } + _featureOut && _featureOut.apply(this, arguments); + self.featureOut && self.featureOut.apply(this, arguments); + }; + + opts.featureClick = _.debounce(function() { + _featureClick && _featureClick.apply(this, arguments); + self.featureClick && self.featureClick.apply(opts, arguments); + }, 10); + + + //CartoDBLayerGroup.call(this, opts); + base.call(this, opts); + cdb.geo.GMapsLayerView.call(this, layerModel, this, gmapsMap); + }; + + _.extend( + GMapsCartoDBLayerGroupView.prototype, + cdb.geo.GMapsLayerView.prototype, + base.prototype, + { + + _update: function() { + this.setOptions(this.model.attributes); + }, + + reload: function() { + this.model.invalidate(); + }, + + remove: function() { + cdb.geo.GMapsLayerView.prototype.remove.call(this); + this.clear(); + }, + + featureOver: function(e, latlon, pixelPos, data, layer) { + // dont pass gmaps LatLng + this.trigger('featureOver', e, [latlon.lat(), latlon.lng()], pixelPos, data, layer); + }, + + featureOut: function(e, layer) { + this.trigger('featureOut', e, layer); + }, + + featureClick: function(e, latlon, pixelPos, data, layer) { + // dont pass leaflet lat/lon + this.trigger('featureClick', e, [latlon.lat(), latlon.lng()], pixelPos, data, layer); + }, + + error: function(e) { + if(this.model) { + //trigger the error form _checkTiles in the model + this.model.trigger('error', e?e.errors:'unknown error'); + this.model.trigger('tileError', e?e.errors:'unknown error'); + } + }, + + ok: function(e) { + this.model.trigger('tileOk'); + }, + + tilesOk: function(e) { + this.model.trigger('tileOk'); + }, + + loading: function() { + this.trigger("loading"); + }, + + finishLoading: function() { + this.trigger("load"); + } + + + }); + return GMapsCartoDBLayerGroupView; +} + +cdb.geo.GMapsCartoDBLayerGroupView = LayerGroupView(CartoDBLayerGroup); +cdb.geo.GMapsCartoDBNamedMapView = LayerGroupView(CartoDBNamedMap); + +cdb.geo.CartoDBNamedMapGMaps = CartoDBNamedMap; +/** +* gmaps cartodb layer +*/ + +})(); + +(function() { +// if google maps is not defined do not load the class +if(typeof(google) == "undefined" || typeof(google.maps) == "undefined") + return; + +// helper to get pixel position from latlon +var Projector = function(map) { this.setMap(map); }; +Projector.prototype = new google.maps.OverlayView(); +Projector.prototype.draw = function() {}; +Projector.prototype.latLngToPixel = function(point) { + var p = this.getProjection(); + if(p) { + return p.fromLatLngToContainerPixel(point); + } + return [0, 0]; +}; +Projector.prototype.pixelToLatLng = function(point) { + var p = this.getProjection(); + if(p) { + return p.fromContainerPixelToLatLng(point); + } + return [0, 0]; + //return this.map.getProjection().fromPointToLatLng(point); +}; + +var CartoDBLayer = function(options) { + + var default_options = { + query: "SELECT * FROM {{table_name}}", + opacity: 0.99, + attribution: cdb.config.get('cartodb_attributions'), + opacity: 1, + debug: false, + visible: true, + added: false, + extra_params: {}, + layer_definition_version: '1.0.0' + }; + + this.options = _.defaults(options, default_options); + + if (!options.table_name || !options.user_name || !options.tile_style) { + throw ('cartodb-gmaps needs at least a CartoDB table name, user_name and tile_style'); + } + + + this.options.layer_definition = { + version: this.options.layer_definition_version, + layers: [{ + type: 'cartodb', + options: this._getLayerDefinition(), + infowindow: this.options.infowindow + }] + }; + cdb.geo.CartoDBLayerGroupGMaps.call(this, this.options); + + this.setOptions(this.options); + +}; + +_.extend(CartoDBLayer.prototype, cdb.geo.CartoDBLayerGroupGMaps.prototype); + +CartoDBLayer.prototype.setQuery = function (layer, sql) { + if(sql === undefined) { + sql = layer; + layer = 0; + } + sql = sql || 'select * from ' + this.options.table_name; + LayerDefinition.prototype.setQuery.call(this, layer, sql); +}; + +cdb.geo.CartoDBLayerGMaps = CartoDBLayer; + +/** +* gmaps cartodb layer +*/ + +var GMapsCartoDBLayerView = function(layerModel, gmapsMap) { + var self = this; + + _.bindAll(this, 'featureOut', 'featureOver', 'featureClick'); + + var opts = _.clone(layerModel.attributes); + + opts.map = gmapsMap; + + var // preserve the user's callbacks + _featureOver = opts.featureOver, + _featureOut = opts.featureOut, + _featureClick = opts.featureClick; + + opts.featureOver = function() { + _featureOver && _featureOver.apply(this, arguments); + self.featureOver && self.featureOver.apply(this, arguments); + }; + + opts.featureOut = function() { + _featureOut && _featureOut.apply(this, arguments); + self.featureOut && self.featureOut.apply(this, arguments); + }; + + opts.featureClick = function() { + _featureClick && _featureClick.apply(this, arguments); + self.featureClick && self.featureClick.apply(opts, arguments); + }; + + cdb.geo.CartoDBLayerGMaps.call(this, opts); + cdb.geo.GMapsLayerView.call(this, layerModel, this, gmapsMap); +}; + +cdb.geo.GMapsCartoDBLayerView = GMapsCartoDBLayerView; + + +_.extend( + GMapsCartoDBLayerView.prototype, + cdb.geo.CartoDBLayerGMaps.prototype, + cdb.geo.GMapsLayerView.prototype, + { + + _update: function() { + this.setOptions(this.model.attributes); + }, + + reload: function() { + this.model.invalidate(); + }, + + remove: function() { + cdb.geo.GMapsLayerView.prototype.remove.call(this); + this.clear(); + }, + + featureOver: function(e, latlon, pixelPos, data) { + // dont pass gmaps LatLng + this.trigger('featureOver', e, [latlon.lat(), latlon.lng()], pixelPos, data, 0); + }, + + featureOut: function(e) { + this.trigger('featureOut', e); + }, + + featureClick: function(e, latlon, pixelPos, data, layer) { + // dont pass leaflet lat/lon + this.trigger('featureClick', e, [latlon.lat(), latlon.lng()], pixelPos, data, 0); + }, + + error: function(e) { + if(this.model) { + //trigger the error form _checkTiles in the model + this.model.trigger('error', e?e.error:'unknown error'); + this.model.trigger('tileError', e?e.error:'unknown error'); + } + }, + + tilesOk: function(e) { + this.model.trigger('tileOk'); + }, + + loading: function() { + this.trigger("loading"); + }, + + finishLoading: function() { + this.trigger("load"); + } + + +}); + +})(); + +(function() { +/** + * view for markers + */ +function PointView(geometryModel) { + var self = this; + // events to link + var events = [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'dragstart', + 'drag', + 'dragend' + ]; + + this._eventHandlers = {}; + this.model = geometryModel; + this.points = []; + + var style = _.clone(geometryModel.get('style')) || {}; + var iconAnchor = this.model.get('iconAnchor'); + + var icon = { + url: this.model.get('iconUrl') || cdb.config.get('assets_url') + '/images/layout/default_marker.png', + anchor: { + x: iconAnchor && iconAnchor[0] || 10, + y: iconAnchor && iconAnchor[1] || 10, + } + }; + + this.geom = new GeoJSON ( + geometryModel.get('geojson'), + { + icon: icon, + raiseOnDrag: false, + crossOnDrag: false + } + ); + + // bind events + var i; + for(i = 0; i < events.length; ++i) { + var e = events[i]; + google.maps.event.addListener(this.geom, e, self._eventHandler(e)); + } + + // link dragging + this.bind('dragend', function(e, pos) { + geometryModel.set({ + geojson: { + type: 'Point', + // geojson is lng,lat + coordinates: [pos[1], pos[0]] + } + }); + }); +} + +PointView.prototype = new GeometryView(); + +PointView.prototype._eventHandler = function(evtType) { + var self = this; + var h = this._eventHandlers[evtType]; + if(!h) { + h = function(e) { + var latlng = e.latLng; + var s = [latlng.lat(), latlng.lng()]; + self.trigger(evtType, e, s); + }; + this._eventHandlers[evtType] = h; + } + return h; +}; + +PointView.prototype.edit = function(enable) { + this.geom.setDraggable(enable); +}; + +/** + * view for other geometries (polygons/lines) + */ +function PathView(geometryModel) { + var self = this; + // events to link + var events = [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + ]; + + this._eventHandlers = {}; + this.model = geometryModel; + this.points = []; + + + + var style = _.clone(geometryModel.get('style')) || {}; + + this.geom = new GeoJSON ( + geometryModel.get('geojson'), + style + ); + + /*_.each(this.geom._layers, function(g) { + g.setStyle(geometryModel.get('style')); + g.on('edit', function() { + geometryModel.set('geojson', L.GeoJSON.toGeoJSON(self.geom)); + }, self); + }); + */ + + _.bindAll(this, '_updateModel'); + var self = this; + + function bindPath(p) { + google.maps.event.addListener(p, 'insert_at', self._updateModel); + /* + google.maps.event.addListener(p, 'remove_at', this._updateModel); + google.maps.event.addListener(p, 'set_at', this._updateModel); + */ + } + + // TODO: check this conditions + + if(this.geom.getPaths) { + var paths = this.geom.getPaths(); + + if (paths && paths[0]) { + // More than one path + for(var i = 0; i < paths.length; ++i) { + bindPath(paths[i]); + } + } else { + // One path + bindPath(paths); + google.maps.event.addListener(this.geom, 'mouseup', this._updateModel); + } + } else { + // More than one path + if (this.geom.length) { + for(var i = 0; i < this.geom.length; ++i) { + bindPath(this.geom[i].getPath()); + google.maps.event.addListener(this.geom[i], 'mouseup', this._updateModel); + } + } else { + // One path + bindPath(this.geom.getPath()); + google.maps.event.addListener(this.geom, 'mouseup', this._updateModel); + } + } + + /*for(var i = 0; i < events.length; ++i) { + var e = events[i]; + this.geom.on(e, self._eventHandler(e)); + }*/ + +} + +PathView.prototype = new GeometryView(); + +PathView.getGeoJSON = function(geom, gType) { + + var coordFn = { + 'Polygon': 'getPath', + 'MultiPolygon': 'getPath', + 'LineString': 'getPath', + 'MultiLineString': 'getPath', + 'Point': 'getPosition', + 'MultiPoint': 'getPosition' + }; + + function _coord(latlng) { + return [latlng.lng(), latlng.lat()]; + } + + function _coords(latlngs) { + var c = []; + for(var i = 0; i < latlngs.length; ++i) { + c.push(_coord(latlngs.getAt(i))); + } + return c; + } + + // single + if(!geom.length || geom.length == 1) { + var g = geom.length ? geom[0]: geom; + var coords; + if(gType == 'Point') { + coords = _coord(g.getPosition()); + } else if(gType == 'MultiPoint') { + coords = [_coord(g.getPosition())] + } else if(gType == 'Polygon') { + coords = [_coords(g.getPath())]; + coords[0].push(_coord(g.getPath().getAt(0))); + } else if(gType == 'MultiPolygon') { + coords = []; + for(var p = 0; p < g.getPaths().length; ++p) { + var c = _coords(g.getPaths().getAt(p)); + c.push(_coord(g.getPaths().getAt(p).getAt(0))); + coords.push(c); + } + coords = [coords] + } else if(gType == 'LineString') { + coords = _coords(g.getPath()); + } else if(gType == 'MultiLineString') { + //TODO: redo + coords = [_coords(g.getPath())]; + } + return { + type: gType, + coordinates: coords + } + } else { + // poly + var c = []; + for(var i = 0; i < geom.length; ++i) { + c.push(PathView.getGeoJSON(geom[i], gType).coordinates[0]); + } + return { + type: gType, + coordinates: c + } + } +} + +PathView.prototype._updateModel = function(e) { + var self = this; + setTimeout(function() { + self.model.set('geojson', PathView.getGeoJSON(self.geom, self.model.get('geojson').type )); + }, 100) +} + +PathView.prototype.edit = function(enable) { + + var fn = enable ? 'enable': 'disable'; + var g = this.geom.length ? this.geom: [this.geom]; + for(var i = 0; i < g.length; ++i) { + g[i].setEditable(enable); + } + if(!enable) { + this.model.set('geojson', PathView.getGeoJSON(this.geom, this.model.get('geojson').type)); + } +}; + +cdb.geo.gmaps = cdb.geo.gmaps || {}; + +cdb.geo.gmaps.PointView = PointView; +cdb.geo.gmaps.PathView = PathView; + + + +})(); + + +// if google maps is not defined do not load the class +if(typeof(google) != "undefined" && typeof(google.maps) != "undefined") { + + var DEFAULT_MAP_STYLE = [ { stylers: [ { saturation: -65 }, { gamma: 1.52 } ] },{ featureType: "administrative", stylers: [ { saturation: -95 }, { gamma: 2.26 } ] },{ featureType: "water", elementType: "labels", stylers: [ { visibility: "off" } ] },{ featureType: "administrative.locality", stylers: [ { visibility: "off" } ] },{ featureType: "road", stylers: [ { visibility: "simplified" }, { saturation: -99 }, { gamma: 2.22 } ] },{ featureType: "poi", elementType: "labels", stylers: [ { visibility: "off" } ] },{ featureType: "road.arterial", stylers: [ { visibility: "off" } ] },{ featureType: "road.local", elementType: "labels", stylers: [ { visibility: "off" } ] },{ featureType: "transit", stylers: [ { visibility: "off" } ] },{ featureType: "road", elementType: "labels", stylers: [ { visibility: "off" } ] },{ featureType: "poi", stylers: [ { saturation: -55 } ] } ]; + + + + cdb.geo.GoogleMapsMapView = cdb.geo.MapView.extend({ + + layerTypeMap: { + "tiled": cdb.geo.GMapsTiledLayerView, + "cartodb": cdb.geo.GMapsCartoDBLayerView, + "carto": cdb.geo.GMapsCartoDBLayerView, + "plain": cdb.geo.GMapsPlainLayerView, + "gmapsbase": cdb.geo.GMapsBaseLayerView, + "layergroup": cdb.geo.GMapsCartoDBLayerGroupView, + "namedmap": cdb.geo.GMapsCartoDBNamedMapView, + "torque": function(layer, map) { + return new cdb.geo.GMapsTorqueLayerView(layer, map); + }, + "wms": cdb.geo.LeafLetWMSLayerView + }, + + initialize: function() { + _.bindAll(this, '_ready'); + this._isReady = false; + var self = this; + + cdb.geo.MapView.prototype.initialize.call(this); + + var bounds = this.map.getViewBounds(); + + if (bounds) { + this.showBounds(bounds); + } + + var center = this.map.get('center'); + + if (!this.options.map_object) { + + this.map_googlemaps = new google.maps.Map(this.el, { + center: new google.maps.LatLng(center[0], center[1]), + zoom: this.map.get('zoom'), + minZoom: this.map.get('minZoom'), + maxZoom: this.map.get('maxZoom'), + disableDefaultUI: true, + scrollwheel: this.map.get("scrollwheel"), + mapTypeControl:false, + mapTypeId: google.maps.MapTypeId.ROADMAP, + backgroundColor: 'white', + tilt: 0 + }); + + this.map.bind('change:maxZoom', function() { + self.map_googlemaps.setOptions({ maxZoom: self.map.get('maxZoom') }); + }, this); + + this.map.bind('change:minZoom', function() { + self.map_googlemaps.setOptions({ minZoom: self.map.get('minZoom') }); + }, this); + + } else { + + this.map_googlemaps = this.options.map_object; + this.setElement(this.map_googlemaps.getDiv()); + + // fill variables + var c = self.map_googlemaps.getCenter(); + + self._setModelProperty({ center: [c.lat(), c.lng()] }); + self._setModelProperty({ zoom: self.map_googlemaps.getZoom() }); + + // unset bounds to not change mapbounds + self.map.unset('view_bounds_sw', { silent: true }); + self.map.unset('view_bounds_ne', { silent: true }); + + } + + this.map.geometries.bind('add', this._addGeometry, this); + this.map.geometries.bind('remove', this._removeGeometry, this); + + + this._bindModel(); + this._addLayers(); + this.setAttribution(); + + google.maps.event.addListener(this.map_googlemaps, 'center_changed', function() { + var c = self.map_googlemaps.getCenter(); + self._setModelProperty({ center: [c.lat(), c.lng()] }); + }); + + google.maps.event.addListener(this.map_googlemaps, 'zoom_changed', function() { + self._setModelProperty({ + zoom: self.map_googlemaps.getZoom() + }); + }); + + google.maps.event.addListener(this.map_googlemaps, 'click', function(e) { + self.trigger('click', e, [e.latLng.lat(), e.latLng.lng()]); + }); + + google.maps.event.addListener(this.map_googlemaps, 'dragend', function(e) { + var c = self.map_googlemaps.getCenter(); + self.trigger('dragend', e, [c.lat(), c.lng()]); + }); + + google.maps.event.addListener(this.map_googlemaps, 'dblclick', function(e) { + self.trigger('dblclick', e); + }); + + this.map.layers.bind('add', this._addLayer, this); + this.map.layers.bind('remove', this._removeLayer, this); + this.map.layers.bind('reset', this._addLayers, this); + this.map.layers.bind('change:type', this._swicthLayerView, this); + + this.projector = new cdb.geo.CartoDBLayerGroupGMaps.Projector(this.map_googlemaps); + + this.projector.draw = this._ready; + }, + + _ready: function() { + this.projector.draw = function() {}; + this.trigger('ready'); + this._isReady = true; + }, + + _setKeyboard: function(model, z) { + this.map_googlemaps.setOptions({ keyboardShortcuts: z }); + }, + + _setScrollWheel: function(model, z) { + this.map_googlemaps.setOptions({ scrollwheel: z }); + }, + + _setZoom: function(model, z) { + z = z || 0; + this.map_googlemaps.setZoom(z); + }, + + _setCenter: function(model, center) { + var c = new google.maps.LatLng(center[0], center[1]); + this.map_googlemaps.setCenter(c); + }, + + createLayer: function(layer) { + var layer_view, + layerClass = this.layerTypeMap[layer.get('type').toLowerCase()]; + + if (layerClass) { + try { + layer_view = new layerClass(layer, this.map_googlemaps); + } catch(e) { + cdb.log.error("MAP: error creating '" + layer.get('type') + "' layer -> " + e.message); + } + } else { + cdb.log.error("MAP: " + layer.get('type') + " can't be created"); + } + return layer_view; + }, + + _addLayer: function(layer, layers, opts) { + opts = opts || {}; + var self = this; + var lyr, layer_view; + + layer_view = this.createLayer(layer); + + if (!layer_view) { + return; + } + return this._addLayerToMap(layer_view, opts); + }, + + _addLayerToMap: function(layer_view, opts) { + var layer = layer_view.model; + + this.layers[layer.cid] = layer_view; + + if (layer_view) { + var idx = _(this.layers).filter(function(lyr) { return !!lyr.getTile; }).length - 1; + var isBaseLayer = _.keys(this.layers).length === 1 || (opts && opts.index === 0) || layer.get('order') === 0; + // set base layer + if(isBaseLayer && !opts.no_base_layer) { + var m = layer_view.model; + if(m.get('type') === 'GMapsBase') { + layer_view._update(); + } else { + layer_view.isBase = true; + layer_view._update(); + } + } else { + idx -= 1; + idx = Math.max(0, idx); // avoid -1 + if (layer_view.getTile) { + if (!layer_view.gmapsLayer) { + cdb.log.error("gmaps layer can't be null"); + } + this.map_googlemaps.overlayMapTypes.setAt(idx, layer_view.gmapsLayer); + } else { + layer_view.gmapsLayer.setMap(this.map_googlemaps); + } + } + if(opts === undefined || !opts.silent) { + this.trigger('newLayerView', layer_view, layer, this); + } + } else { + cdb.log.error("layer type not supported"); + } + + return layer_view; + }, + + pixelToLatLon: function(pos) { + var latLng = this.projector.pixelToLatLng(new google.maps.Point(pos[0], pos[1])); + return { + lat: latLng.lat(), + lng: latLng.lng() + } + }, + + latLonToPixel: function(latlon) { + return this.projector.latLngToPixel(new google.maps.LatLng(latlon[0], latlon[1])); + }, + + getSize: function() { + return { + x: this.$el.width(), + y: this.$el.height() + }; + }, + + panBy: function(p) { + var c = this.map.get('center'); + var pc = this.latLonToPixel(c); + p.x += pc.x; + p.y += pc.y; + var ll = this.projector.pixelToLatLng(p); + this.map.setCenter([ll.lat(), ll.lng()]); + }, + + getBounds: function() { + if(this._isReady) { + var b = this.map_googlemaps.getBounds(); + var sw = b.getSouthWest(); + var ne = b.getNorthEast(); + return [ + [sw.lat(), sw.lng()], + [ne.lat(), ne.lng()] + ]; + } + return [ [0,0], [0,0] ]; + }, + + setAttribution: function() { + // Remove old one + var old = document.getElementById("cartodb-gmaps-attribution") + , attribution = this.map.get("attribution").join(", "); + + // If div already exists, remove it + if (old) { + old.parentNode.removeChild(old); + } + + // Add new one + var container = this.map_googlemaps.getDiv() + , cartodb_attribution = document.createElement("div"); + + cartodb_attribution.setAttribute('id','cartodb-gmaps-attribution'); + cartodb_attribution.setAttribute('class', 'gmaps'); + container.appendChild(cartodb_attribution); + cartodb_attribution.innerHTML = attribution; + }, + + setCursor: function(cursor) { + this.map_googlemaps.setOptions({ draggableCursor: cursor }); + }, + + _addGeomToMap: function(geom) { + var geo = cdb.geo.GoogleMapsMapView.createGeometry(geom); + if(geo.geom.length) { + for(var i = 0 ; i < geo.geom.length; ++i) { + geo.geom[i].setMap(this.map_googlemaps); + } + } else { + geo.geom.setMap(this.map_googlemaps); + } + return geo; + }, + + _removeGeomFromMap: function(geo) { + if(geo.geom.length) { + for(var i = 0 ; i < geo.geom.length; ++i) { + geo.geom[i].setMap(null); + } + } else { + geo.geom.setMap(null); + } + }, + + getNativeMap: function() { + return this.map_googlemaps; + }, + + invalidateSize: function() { + google.maps.event.trigger(this.map_googlemaps, 'resize'); + } + + }, { + + addLayerToMap: function(layer, map, pos) { + pos = pos || 0; + if (!layer) { + cdb.log.error("gmaps layer can't be null"); + } + if (layer.getTile) { + map.overlayMapTypes.setAt(pos, layer); + } else { + layer.setMap(map); + } + }, + + /** + * create the view for the geometry model + */ + createGeometry: function(geometryModel) { + if(geometryModel.isPoint()) { + return new cdb.geo.gmaps.PointView(geometryModel); + } + return new cdb.geo.gmaps.PathView(geometryModel); + } + }); + +} + +/** + * generic dialog + * + * this opens a dialog in the middle of the screen rendering + * a dialog using cdb.templates 'common/dialog' or template_base option. + * + * inherit class should implement render_content (it could return another widget) + * + * usage example: + * + * var MyDialog = cdb.ui.common.Dialog.extend({ + * render_content: function() { + * return "my content"; + * }, + * }) + * var dialog = new MyDialog({ + * title: 'test', + * description: 'long description here', + * template_base: $('#base_template').html(), + * width: 500 + * }); + * + * $('body').append(dialog.render().el); + * dialog.open(); + * + * TODO: implement draggable + * TODO: modal + * TODO: document modal_type + */ + +cdb.ui.common.Dialog = cdb.core.View.extend({ + + tagName: 'div', + className: 'dialog', + + events: { + 'click .ok': '_ok', + 'click .cancel': '_cancel', + 'click .close': '_cancel' + }, + + default_options: { + title: 'title', + description: '', + ok_title: 'Ok', + cancel_title: 'Cancel', + width: 300, + height: 200, + clean_on_hide: false, + enter_to_confirm: false, + template_name: 'old_common/views/dialog_base', + ok_button_classes: 'button green', + cancel_button_classes: '', + modal_type: '', + modal_class: '', + include_footer: true, + additionalButtons: [] + }, + + initialize: function() { + _.defaults(this.options, this.default_options); + + _.bindAll(this, 'render', '_keydown'); + + // Keydown bindings for the dialog + $(document).bind('keydown', this._keydown); + + // After removing the dialog, cleaning other bindings + this.bind("clean", this._reClean); + + this.template_base = this.options.template_base ? _.template(this.options.template_base) : cdb.templates.getTemplate(this.options.template_name); + }, + + render: function() { + var $el = this.$el; + + $el.html(this.template_base(this.options)); + + $el.find(".modal").css({ + width: this.options.width + //height: this.options.height + //'margin-left': -this.options.width>>1, + //'margin-top': -this.options.height>>1 + }); + + if(this.render_content) { + + this.$('.content').append(this.render_content()); + } + + if(this.options.modal_class) { + this.$el.addClass(this.options.modal_class); + } + + return this; + }, + + + _keydown: function(e) { + // If clicks esc, goodbye! + if (e.keyCode === 27) { + this._cancel(); + // If clicks enter, same as you click on ok button. + } else if (e.keyCode === 13 && this.options.enter_to_confirm) { + this._ok(); + } + }, + + /** + * helper method that renders the dialog and appends it to body + */ + appendToBody: function() { + $('body').append(this.render().el); + return this; + }, + + _ok: function(ev) { + + if(ev) ev.preventDefault(); + + if (this.ok) { + this.ok(this.result); + } + + this.hide(); + + }, + + _cancel: function(ev) { + + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + + if (this.cancel) { + this.cancel(); + } + + this.hide(); + + }, + + hide: function() { + + this.$el.hide(); + + if (this.options.clean_on_hide) { + this.clean(); + } + + }, + + open: function() { + + this.$el.show(); + + }, + + _reClean: function() { + + $(document).unbind('keydown', this._keydown); + + } + +}); + + +cdb.ui.common.ShareDialog = cdb.ui.common.Dialog.extend({ + + tagName: 'div', + className: 'cartodb-share-dialog', + + events: { + 'click .ok': '_ok', + 'click .cancel': '_cancel', + 'click .close': '_cancel', + "click": '_stopPropagation', + "dblclick": '_stopPropagation', + "mousedown": '_stopPropagation' + }, + + default_options: { + title: '', + description: '', + ok_title: 'Ok', + cancel_title: 'Cancel', + width: 300, + height: 200, + clean_on_hide: false, + enter_to_confirm: false, + template_name: 'old_common/views/dialog_base', + ok_button_classes: 'button green', + cancel_button_classes: '', + modal_type: '', + modal_class: '', + include_footer: true, + additionalButtons: [] + }, + + initialize: function() { + + _.defaults(this.options, this.default_options); + + _.bindAll(this, 'render', '_keydown'); + + this.isOpen = false; + + var self = this; + + if (this.options.target) { + this.options.target.on("click", function(e) { + e.preventDefault(); + e.stopPropagation(); + + self.open(); + + }) + } + + // Keydown bindings for the dialog + $(document).bind('keydown', this._keydown); + + // After removing the dialog, cleaning other bindings + this.bind("clean", this._reClean); + + }, + + _stopPropagation: function(ev) { + + ev.stopPropagation(); + + }, + + _stripHTML: function(input, allowed) { + + allowed = (((allowed || "") + "").toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join(''); + + var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi; + + if (!input || (typeof input != "string")) return ''; + + return input.replace(tags, function ($0, $1) { + return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; + }); + + }, + + open: function() { + + var self = this; + + this.$el.show(0, function(){ + self.isOpen = true; + }); + + }, + + hide: function() { + + var self = this; + + this.$el.hide(0, function(){ + self.isOpen = false; + }); + + if (this.options.clean_on_hide) { + this.clean(); + } + + }, + + toggle: function() { + + if (this.isOpen) { + this.hide(); + } else { + this.open(); + } + + }, + + _truncateTitle: function(s, length) { + + return s.substr(0, length-1) + (s.length > length ? '…' : ''); + + }, + + render: function() { + + var $el = this.$el; + + var title = cdb.core.sanitize.html(this.options.title); + var description = cdb.core.sanitize.html(this.options.description); + var clean_description = this._stripHTML(this.options.description); + var share_url = this.options.share_url; + + var facebook_url, twitter_url; + + this.$el.addClass(this.options.size); + + var full_title = title + ": " + clean_description; + var twitter_title; + + if (title && clean_description) { + twitter_title = this._truncateTitle(title + ": " + clean_description, 112) + " %23map " + } else if (title) { + twitter_title = this._truncateTitle(title, 112) + " %23map" + } else if (clean_description){ + twitter_title = this._truncateTitle(clean_description, 112) + " %23map" + } else { + twitter_title = "%23map" + } + + if (this.options.facebook_url) { + facebook_url = this.options.facebook_url; + } else { + facebook_url = "http://www.facebook.com/sharer.php?u=" + share_url + "&text=" + full_title; + } + + if (this.options.twitter_url) { + twitter_url = this.options.twitter_url; + } else { + twitter_url = "https://twitter.com/share?url=" + share_url + "&text=" + twitter_title; + } + + var options = _.extend(this.options, { facebook_url: facebook_url, twitter_url: twitter_url }); + + $el.html(this.options.template(options)); + + $el.find(".modal").css({ + width: this.options.width + }); + + if (this.render_content) { + this.$('.content').append(this.render_content()); + } + + if(this.options.modal_class) { + this.$el.addClass(this.options.modal_class); + } + + if (this.options.disableLinks) { + this.$el.find("a").attr("target", ""); + } + + return this; + } + +}); + +/** + * generic embbed notification, like twitter "new notifications" + * + * it shows slowly the notification with a message and a close button. + * Optionally you can set a timeout to close + * + * usage example: + * + var notification = new cdb.ui.common.Notificaiton({ + el: "#notification_element", + msg: "error!", + timeout: 1000 + }); + notification.show(); + // close it + notification.close(); +*/ + +cdb.ui.common.Notification = cdb.core.View.extend({ + + tagName: 'div', + className: 'dialog', + + events: { + 'click .close': 'hide' + }, + + default_options: { + timeout: 0, + msg: '', + hideMethod: '', + duration: 'normal' + }, + + initialize: function() { + this.closeTimeout = -1; + _.defaults(this.options, this.default_options); + this.template = this.options.template ? _.template(this.options.template) : cdb.templates.getTemplate('common/notification'); + + this.$el.hide(); + }, + + render: function() { + var $el = this.$el; + $el.html(this.template(this.options)); + if(this.render_content) { + this.$('.content').append(this.render_content()); + } + return this; + }, + + hide: function(ev) { + var self = this; + if (ev) + ev.preventDefault(); + clearTimeout(this.closeTimeout); + if(this.options.hideMethod != '' && this.$el.is(":visible") ) { + this.$el[this.options.hideMethod](this.options.duration, 'swing', function() { + self.$el.html(''); + self.trigger('notificationDeleted'); + self.remove(); + }); + } else { + this.$el.hide(); + self.$el.html(''); + self.trigger('notificationDeleted'); + self.remove(); + } + + }, + + open: function(method, options) { + this.render(); + this.$el.show(method, options); + if(this.options.timeout) { + this.closeTimeout = setTimeout(_.bind(this.hide, this), this.options.timeout); + } + } + +}); + + +/** + * generic table + * + * this class creates a HTML table based on Table model (see below) and modify it based on model changes + * + * usage example: + * + var table = new Table({ + model: table + }); + + $('body').append(table.render().el); + + * model should be a collection of Rows + + */ + +/** + * represents a table row + */ +cdb.ui.common.Row = cdb.core.Model.extend({ +}); + +cdb.ui.common.TableData = Backbone.Collection.extend({ + model: cdb.ui.common.Row, + fetched: false, + + initialize: function() { + var self = this; + this.bind('reset', function() { + self.fetched = true; + }) + }, + + /** + * get value for row index and columnName + */ + getCell: function(index, columnName) { + var r = this.at(index); + if(!r) { + return null; + } + return r.get(columnName); + }, + + isEmpty: function() { + return this.length === 0; + } + +}); + +/** + * contains information about the table, mainly the schema + */ +cdb.ui.common.TableProperties = cdb.core.Model.extend({ + + columnNames: function() { + return _.map(this.get('schema'), function(c) { + return c[0]; + }); + }, + + columnName: function(idx) { + return this.columnNames()[idx]; + } +}); + +/** + * renders a table row + */ +cdb.ui.common.RowView = cdb.core.View.extend({ + tagName: 'tr', + + initialize: function() { + + this.model.bind('change', this.render, this); + this.model.bind('destroy', this.clean, this); + this.model.bind('remove', this.clean, this); + this.model.bind('change', this.triggerChange, this); + this.model.bind('sync', this.triggerSync, this); + this.model.bind('error', this.triggerError, this); + + this.add_related_model(this.model); + this.order = this.options.order; + }, + + triggerChange: function() { + this.trigger('changeRow'); + }, + + triggerSync: function() { + this.trigger('syncRow'); + }, + + triggerError: function() { + this.trigger('errorRow') + }, + + valueView: function(colName, value) { + return value; + }, + + render: function() { + var self = this; + var row = this.model; + + var tr = ''; + + var tdIndex = 0; + var td; + if(this.options.row_header) { + td = ''; + } else { + td = ''; + } + var v = self.valueView('', ''); + if(v.html) { + v = v[0].outerHTML; + } + td += v; + td += ''; + tdIndex++; + tr += td + + var attrs = this.order || _.keys(row.attributes); + var tds = ''; + var row_attrs = row.attributes; + for(var i = 0, len = attrs.length; i < len; ++i) { + var key = attrs[i]; + var value = row_attrs[key]; + if(value !== undefined) { + var td = ''; + var v = self.valueView(key, value); + if(v.html) { + v = v[0].outerHTML; + } + td += v; + td += ''; + tdIndex++; + tds += td; + } + } + tr += tds; + this.$el.html(tr).attr('id', 'row_' + row.id); + return this; + }, + + getCell: function(x) { + var childNo = x; + if(this.options.row_header) { + ++x; + } + return this.$('td:eq(' + x + ')'); + }, + + getTableView: function() { + return this.tableView; + } + +}); + +/** + * render a table + * this widget needs two data sources + * - the table model which contains information about the table (columns and so on). See TableProperties + * - the model with the data itself (TableData) + */ +cdb.ui.common.Table = cdb.core.View.extend({ + + tagName: 'table', + rowView: cdb.ui.common.RowView, + + events: { + 'click td': '_cellClick', + 'dblclick td': '_cellDblClick' + }, + + default_options: { + }, + + initialize: function() { + var self = this; + _.defaults(this.options, this.default_options); + this.dataModel = this.options.dataModel; + this.rowViews = []; + + // binding + this.setDataSource(this.dataModel); + this.model.bind('change', this.render, this); + this.model.bind('change:dataSource', this.setDataSource, this); + + // assert the rows are removed when table is removed + this.bind('clean', this.clear_rows, this); + + // prepare for cleaning + this.add_related_model(this.dataModel); + this.add_related_model(this.model); + + // we need to use custom signals to make the tableview aware of a row being deleted, + // because when you delete a point from the map view, sometimes it isn't on the dataModel + // collection, so its destroy doesn't bubble throught there. + // Also, the only non-custom way to acknowledge that a row has been correctly deleted from a server is with + // a sync, that doesn't bubble through the table + this.model.bind('removing:row', function() { + self.rowsBeingDeleted = self.rowsBeingDeleted ? self.rowsBeingDeleted +1 : 1; + self.rowDestroying(); + }); + this.model.bind('remove:row', function() { + if(self.rowsBeingDeleted > 0) { + self.rowsBeingDeleted--; + self.rowDestroyed(); + if(self.dataModel.length == 0) { + self.emptyTable(); + } + } + }); + + }, + + headerView: function(column) { + return column[0]; + }, + + setDataSource: function(dm) { + if(this.dataModel) { + this.dataModel.unbind(null, null, this); + } + this.dataModel = dm; + this.dataModel.bind('reset', this._renderRows, this); + this.dataModel.bind('error', this._renderRows, this); + this.dataModel.bind('add', this.addRow, this); + }, + + _renderHeader: function() { + var self = this; + var thead = $(""); + var tr = $(""); + if(this.options.row_header) { + tr.append($("").append(self.headerView(['', 'header']))); + } else { + tr.append($("").append(self.headerView(['', 'header']))); + } + _(this.model.get('schema')).each(function(col) { + tr.append($("").append(self.headerView(col))); + }); + thead.append(tr); + return thead; + }, + + /** + * remove all rows + */ + clear_rows: function() { + this.$('tfoot').remove(); + this.$('tr.noRows').remove(); + + // unbind rows before cleaning them when all are gonna be removed + var rowView = null; + while(rowView = this.rowViews.pop()) { + // this is a hack to avoid all the elements are removed one by one + rowView.unbind(null, null, this); + // each element removes itself from rowViews + rowView.clean(); + } + // clean all the html at the same time + this.rowViews = []; + }, + + /** + * add rows + */ + addRow: function(row, collection, options) { + var self = this; + var tr = new self.rowView({ + model: row, + order: this.model.columnNames(), + row_header: this.options.row_header + }); + tr.tableView = this; + + tr.bind('clean', function() { + var idx = _.indexOf(self.rowViews, tr); + self.rowViews.splice(idx, 1); + // update index + for(var i = idx; i < self.rowViews.length; ++i) { + self.rowViews[i].$el.attr('data-y', i); + } + }, this); + tr.bind('changeRow', this.rowChanged, this); + tr.bind('saved', this.rowSynched, this); + tr.bind('errorSaving', this.rowFailed, this); + tr.bind('saving', this.rowSaving, this); + this.retrigger('saving', tr); + + tr.render(); + if(options && options.index !== undefined && options.index != self.rowViews.length) { + + tr.$el.insertBefore(self.rowViews[options.index].$el); + self.rowViews.splice(options.index, 0, tr); + //tr.$el.attr('data-y', options.index); + // change others view data-y attribute + for(var i = options.index; i < self.rowViews.length; ++i) { + self.rowViews[i].$el.attr('data-y', i); + } + } else { + // at the end + tr.$el.attr('data-y', self.rowViews.length); + self.$el.append(tr.el); + self.rowViews.push(tr); + } + + this.trigger('createRow'); + }, + + /** + * Callback executed when a row change + * @method rowChanged + * @abstract + */ + rowChanged: function() {}, + + /** + * Callback executed when a row is sync + * @method rowSynched + * @abstract + */ + rowSynched: function() {}, + + /** + * Callback executed when a row fails to reach the server + * @method rowFailed + * @abstract + */ + rowFailed: function() {}, + + /** + * Callback executed when a row send a POST to the server + * @abstract + */ + rowSaving: function() {}, + + /** + * Callback executed when a row is being destroyed + * @method rowDestroyed + * @abstract + */ + rowDestroying: function() {}, + + /** + * Callback executed when a row gets destroyed + * @method rowDestroyed + * @abstract + */ + rowDestroyed: function() {}, + + /** + * Callback executed when a row gets destroyed and the table data is empty + * @method emptyTable + * @abstract + */ + emptyTable: function() {}, + + /** + * Checks if the table is empty + * @method isEmptyTable + * @returns boolean + */ + isEmptyTable: function() { + return (this.dataModel.length === 0 && this.dataModel.fetched) + }, + + /** + * render only data rows + */ + _renderRows: function() { + this.clear_rows(); + if(! this.isEmptyTable()) { + if(this.dataModel.fetched) { + var self = this; + + this.dataModel.each(function(row) { + self.addRow(row); + }); + } else { + this._renderLoading(); + } + } else { + this._renderEmpty(); + } + + }, + + _renderLoading: function() { + }, + + _renderEmpty: function() { + }, + + /** + * Method for the children to redefine with the table behaviour when it has no rows. + * @method addEmptyTableInfo + * @abstract + */ + addEmptyTableInfo: function() { + // #to be overwrite by descendant classes + }, + + /** + * render table + */ + render: function() { + var self = this; + + // render header + self.$el.html(self._renderHeader()); + + // render data + self._renderRows(); + + return this; + + }, + + /** + * return jquery cell element of cell x,y + */ + getCell: function(x, y) { + if(this.options.row_header) { + ++y; + } + return this.rowViews[y].getCell(x); + }, + + _cellClick: function(e, evtName) { + evtName = evtName || 'cellClick'; + e.preventDefault(); + var cell = $(e.currentTarget || e.target); + var x = parseInt(cell.attr('data-x'), 10); + var y = parseInt(cell.parent().attr('data-y'), 10); + this.trigger(evtName, e, cell, x, y); + }, + + _cellDblClick: function(e) { + this._cellClick(e, 'cellDblClick'); + } + + +}); + +/** + * Show a dropdown from the target + * + * It shows the several options of the user settings + * + * usage example: + * + * var settings = new cdb.ui.common.Dropdown({ + * el: "#settings_element", + * speedIn: 300, + * speedOut: 200 + * }); + * // show it + * settings.show(); + * // close it + * settings.close(); +*/ + +cdb.ui.common.Dropdown = cdb.core.View.extend({ + + tagName: 'div', + className: 'dropdown', + + events: { + "click ul li a" : "_fireClick" + }, + + default_options: { + width: 160, + speedIn: 150, + speedOut: 300, + vertical_position: "down", + horizontal_position: "right", + tick: "right", + vertical_offset: 0, + horizontal_offset: 0 + }, + + initialize: function() { + _.bindAll(this, "open", "hide", "_handleClick", "_keydown"); + + // Extend options + _.defaults(this.options, this.default_options); + + // Dropdown template + if (this.options.template_base) { + this.template_base = cdb.templates.getTemplate(this.options.template_base); + } else if (this.options.template) { + this.template_base = this.options.template; + } + + // Bind to target + $(this.options.target).bind({"click": this._handleClick}); + + // Bind ESC key + $(document).bind('keydown', this._keydown); + + // Is open flag + this.isOpen = false; + + }, + + render: function() { + // Render + var $el = this.$el; + $el + .html(this.template_base(this.options)) + .css({ + width: this.options.width + }) + return this; + }, + + _handleClick: function(ev) { + //Check if the dropdown is visible to hiding with the click on the target + if (ev){ + ev.preventDefault(); + ev.stopPropagation(); + } + // If visible + if (this.isOpen){ + this.hide(); + }else{ + this.open(); + } + }, + + _keydown: function(e) { + if (e.keyCode === 27) { + this.hide(); + } + }, + + hide: function() { + this.isOpen = false; + this.$el.hide(); + }, + + show: function() { + this.$el.css({ + display: "block", + opacity: 1 + }); + this.isOpen = true; + }, + + open: function(ev,target) { + // Target + var $target = target && $(target) || this.options.target; + this.options.target = $target; + + // Positionate + var targetPos = $target[this.options.position || 'offset']() + , targetWidth = $target.outerWidth() + , targetHeight = $target.outerHeight() + , elementWidth = this.$el.outerWidth() + , elementHeight = this.$el.outerHeight() + , self = this; + + this.$el.css({ + top: targetPos.top + parseInt((self.options.vertical_position == "up") ? (- elementHeight - 10 - self.options.vertical_offset) : (targetHeight + 10 - self.options.vertical_offset)), + left: targetPos.left + parseInt((self.options.horizontal_position == "left") ? (self.options.horizontal_offset - 15) : (targetWidth - elementWidth + 15 - self.options.horizontal_offset)) + }) + .addClass( + // Add vertical and horizontal position class + (this.options.vertical_position == "up" ? "vertical_top" : "vertical_bottom" ) + + " " + + (this.options.horizontal_position == "right" ? "horizontal_right" : "horizontal_left" ) + + " " + + // Add tick class + "tick_" + this.options.tick + ) + + // Show it + this.show(); + + // Dropdown openned + this.isOpen = true; + }, + + clean: function() { + $(this.options.target).unbind({"click": this._handleClick}); + $(document).unbind('keydown', this._keydown); + cdb.core.View.prototype.clean.apply(this, arguments); + }, + + _fireClick: function(ev) { + this.trigger("optionClicked", ev, this.el); + } +}); + +(function() { + +var _requestCache = {}; + +/** + * defines the container for an overlay. + * It places the overlay + */ +var Overlay = { + + _types: {}, + + // register a type to be created + register: function(type, creatorFn) { + Overlay._types[type] = creatorFn; + }, + + // create a type given the data + // raise an exception if the type does not exist + create: function(type, vis, data) { + var t = Overlay._types[type]; + + if (!t) { + cdb.log.error("Overlay: " + type + " does not exist"); + return; + } + + data.options = typeof data.options === 'string' ? JSON.parse(data.options): data.options; + data.options = data.options || {} + var widget = t(data, vis); + + if (widget) { + widget.type = type; + return widget; + } + + return false; + } +}; + +cdb.vis.Overlay = Overlay; + +cdb.vis.Overlays = Backbone.Collection.extend({ + comparator: function() { + } +}); + +// layer factory +var Layers = { + + _types: {}, + + register: function(type, creatorFn) { + this._types[type] = creatorFn; + }, + + create: function(type, vis, data) { + if (!type) { + cdb.log.error("creating a layer without type"); + return null; + } + var t = this._types[type.toLowerCase()]; + + var c = {}; + c.type = type; + _.extend(c, data, data.options); + return new t(vis, c); + }, + + moduleForLayer: function(type) { + if (type.toLowerCase() === 'torque') { + return 'torque'; + } + return null; + }, + + modulesForLayers: function(layers) { + var modules = _(layers).map(function(layer) { + return Layers.moduleForLayer(layer.type || layer.kind); + }); + return _.compact(_.uniq(modules)); + } + +}; + +cdb.vis.Layers = Layers; + +cartodb.moduleLoad = function(name, mod) { + cartodb[name] = mod; + cartodb.config.modules.add({ + name: name, + mod: mod + }); +}; + +/** + * visulization creation + */ +var Vis = cdb.core.View.extend({ + + initialize: function() { + _.bindAll(this, 'loadingTiles', 'loadTiles', '_onResize'); + + this.https = false; + this.overlays = []; + this.moduleChecked = false; + this.layersing = 0; + + if (this.options.mapView) { + this.mapView = this.options.mapView; + this.map = this.mapView.map; + } + + // recalculate map position on orientation change + if (!window.addEventListener) { + window.attachEvent('orientationchange', this.doOnOrientationChange, this); + } else { + window.addEventListener('orientationchange', _.bind(this.doOnOrientationChange, this)); + } + + }, + + doOnOrientationChange: function() { + //this.setMapPosition(); + }, + + /** + * check if all the modules needed to create layers are loaded + */ + checkModules: function(layers) { + var mods = Layers.modulesForLayers(layers); + return _.every(_.map(mods, function(m) { return cartodb[m] !== undefined; })); + }, + + loadModules: function(layers, done) { + var self = this; + var mods = Layers.modulesForLayers(layers); + for(var i = 0; i < mods.length; ++i) { + Loader.loadModule(mods[i]); + } + function loaded () { + if (self.checkModules(layers)) { + cdb.config.unbind('moduleLoaded', loaded); + done(); + } + } + + cdb.config.bind('moduleLoaded', loaded); + _.defer(loaded); + }, + + _addLegends: function(legends) { + if (this.legends) { + this.legends.remove(); + } + + this.legends = new cdb.geo.ui.StackedLegend({ + legends: legends + }); + + if (!this.mobile_enabled) { + this.mapView.addOverlay(this.legends); + } + }, + + addLegends: function(layers) { + this._addLegends(this.createLegendView(layers)); + }, + + _setLayerOptions: function(options) { + + var layers = []; + + // flatten layers (except baselayer) + var layers = _.map(this.getLayers().slice(1), function(layer) { + if (layer.getSubLayers) { + return layer.getSubLayers(); + } + return layer; + }); + + layers = _.flatten(layers); + + for (i = 0; i < Math.min(options.sublayer_options.length, layers.length); ++i) { + + var o = options.sublayer_options[i]; + var subLayer = layers[i]; + var legend = this.legends && this.legends.getLegendByIndex(i); + + if (legend) { + legend[o.visible ? 'show': 'hide'](); + } + + // HACK + if(subLayer.model && subLayer.model.get('type') === 'torque') { + if (o.visible === false) { + subLayer.model.set('visible', false); + if (this.timeSlider) { + this.timeSlider.hide(); + } + } + } + } + }, + + _addOverlays: function(overlays, data, options) { + + overlays = overlays.toJSON(); + // Sort the overlays by its internal order + overlays = _.sortBy(overlays, function(overlay) { + return overlay.order === null ? Number.MAX_VALUE: overlay.order; + }); + + // clean current overlays + while (this.overlays.length !== 0) { + this.overlays.pop().clean(); + } + + this._createOverlays(overlays, data, options); + }, + + addTimeSlider: function(torqueLayer) { + // if a timeslides already exists don't create it again + if (torqueLayer && (torqueLayer.options.steps > 1) && !this.timeSlider) { + var self = this; + // dont use add overlay since this overlay is managed by torque layer + var timeSlider = Overlay.create('time_slider', this, { layer: torqueLayer }); + this.mapView.addOverlay(timeSlider); + this.timeSlider = timeSlider; + // remove when layer is done + torqueLayer.bind('remove', function _remove() { + self.timeSlider = null; + timeSlider.remove(); + torqueLayer.unbind('remove', _remove); + }); + } + }, + + _setupSublayers: function(layers, options) { + + options.sublayer_options = []; + + _.each(layers.slice(1), function(lyr) { + + if (lyr.type === 'layergroup') { + _.each(lyr.options.layer_definition.layers, function(l) { + options.sublayer_options.push({ visible: ( l.visible !== undefined ? l.visible : true ) }) + }); + } else if (lyr.type === 'namedmap') { + _.each(lyr.options.named_map.layers, function(l) { + options.sublayer_options.push({ visible: ( l.visible !== undefined ? l.visible : true ) }) + }); + } else if (lyr.type === 'torque') { + options.sublayer_options.push({ visible: ( lyr.options.visible !== undefined ? lyr.options.visible : true ) }) + } + + }); + + }, + + load: function(data, options) { + var self = this; + this._data = data; + + if (typeof(data) === 'string') { + var url = data; + + cdb.core.Loader.get(url, function(data) { + if (data) { + self.load(data, options); + } else { + self.throwError('error fetching viz.json file'); + } + }); + + return this; + } + + // if the viz.json contains slides, discard the main viz.json and use the slides + var slides = data.slides; + if (slides && slides.length > 0) { + data = slides[0] + data.slides = slides.slice(1); + } + + // load modules needed for layers + var layers = data.layers; + + // check if there are slides and check all the layers + if (data.slides && data.slides.length > 0) { + layers = layers.concat(_.flatten(data.slides.map(function(s) { return s.layers }))); + } + + if (!this.checkModules(layers)) { + if (this.moduleChecked) { + self.throwError("modules couldn't be loaded"); + return this; + } + + this.moduleChecked = true; + + this.loadModules(layers, function() { + self.load(data, options); + }); + + return this; + } + + // configure the vis in http or https + if (window && window.location.protocol && window.location.protocol === 'https:') { + this.https = true; + } + + if (data.https) { + this.https = data.https; + } + + options = options || {}; + + this._applyOptions(data, options); + + // to know if the logo is enabled search in the overlays and see if logo overlay is included and is shown + var has_logo_overlay = !!_.find(data.overlays, function(o) { return o.type === 'logo' && o.options.display; }); + + this.cartodb_logo = (options.cartodb_logo !== undefined) ? options.cartodb_logo: has_logo_overlay; + + if (this.mobile) this.cartodb_logo = false; + else if (!has_logo_overlay && options.cartodb_logo === undefined) this.cartodb_logo = true; // We set the logo by default + + var scrollwheel = (options.scrollwheel === undefined) ? data.scrollwheel : options.scrollwheel; + var slides_controller = (options.slides_controller === undefined) ? data.slides_controller : options.slides_controller; + + // map + data.maxZoom || (data.maxZoom = 20); + data.minZoom || (data.minZoom = 0); + + //Force using GMaps ? + if ( (this.gmaps_base_type) && (data.map_provider === "leaflet") ) { + + //Check if base_type is correct + var typesAllowed = ['roadmap', 'gray_roadmap', 'dark_roadmap', 'hybrid', 'satellite', 'terrain']; + if (_.contains(typesAllowed, this.gmaps_base_type)) { + if (data.layers) { + data.layers[0].options.type = 'GMapsBase'; + data.layers[0].options.base_type = this.gmaps_base_type; + data.layers[0].options.name = this.gmaps_base_type; + + if (this.gmaps_style) { + data.layers[0].options.style = typeof this.gmaps_style === 'string' ? JSON.parse(this.gmaps_style): this.gmaps_style; + } + + data.map_provider = 'googlemaps'; + data.layers[0].options.attribution = ''; //GMaps has its own attribution + } else { + cdb.log.error('No base map loaded. Using Leaflet.'); + } + } else { + cdb.log.error('GMaps base_type "' + this.gmaps_base_type + ' is not supported. Using leaflet.'); + } + } + + // Create the instance of the cdb.geo.Map model + var mapConfig = { + title: data.title, + description: data.description, + maxZoom: data.maxZoom, + minZoom: data.minZoom, + legends: data.legends, + scrollwheel: scrollwheel, + provider: data.map_provider + }; + + // if the boundaries are defined, we add them to the map + if (data.bounding_box_sw && data.bounding_box_ne) { + mapConfig.bounding_box_sw = data.bounding_box_sw; + mapConfig.bounding_box_ne = data.bounding_box_ne; + } + + if (data.bounds) { + mapConfig.view_bounds_sw = data.bounds[0]; + mapConfig.view_bounds_ne = data.bounds[1]; + } else { + var center = data.center; + + if (typeof(center) === "string") { + center = $.parseJSON(center); + } + + mapConfig.center = center || [0, 0]; + mapConfig.zoom = data.zoom === undefined ? 4: data.zoom; + } + + var map = new cdb.geo.Map(mapConfig); + this.map = map; + this.overlayModels = new Backbone.Collection(); + + this.updated_at = data.updated_at || new Date().getTime(); + + // If a CartoDB embed map is hidden by default, its + // height is 0 and it will need to recalculate its size + // and re-center again. + // We will wait until it is resized and then apply + // the center provided in the parameters and the + // correct size. + var map_h = this.$el.outerHeight(); + + if (map_h === 0) { + this.mapConfig = mapConfig; + $(window).bind('resize', this._onResize); + } + + var div = $('
          ').css({ + position: 'relative', + width: '100%', + height: '100%' + }); + + this.container = div; + + // Another div to prevent leaflet grabbing the div + var div_hack = $('
          ') + .addClass("cartodb-map-wrapper") + .css({ + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + width: '100%' + }); + + div.append(div_hack); + + this.$el.append(div); + + // Create the map + var mapView = new cdb.geo.MapView.create(div_hack, map); + + this.mapView = mapView; + + if (options.legends || (options.legends === undefined && this.map.get("legends") !== false)) { + map.layers.bind('reset', this.addLegends, this); + } + + this.overlayModels.bind('reset', function(overlays) { + this._addOverlays(overlays, data, options); + this._addMobile(data, options); + }, this); + + this.mapView.bind('newLayerView', this._addLoading, this); + + if (options.time_slider) { + this.mapView.bind('newLayerView', this._addTimeSlider, this); + } + + if (this.infowindow) { + this.mapView.bind('newLayerView', this.addInfowindow, this); + } + + if (this.tooltip) { + this.mapView.bind('newLayerView', this.addTooltip, this); + } + + // TODO: We can probably move this logic somewhere in cdb.geo.ui.Widget + var widgetModels = []; + var widgetClasses = { + "list": { + model: 'ListModel', + view: 'List.View' + }, + "histogram": { + model: 'HistogramModel', + view: 'Histogram.View' + } + }; + + _.each(data.widgets, function(widgetData) { + if (!widgetClasses[widgetData.type]) { + throw 'Widget type \'' + widgetData.type + '\' is not supported!'; + } + + // Instantiate the model + var modelClass = widgetClasses[widgetData.type].model; + var widgetModel = new cdb.geo.ui.Widget[modelClass](widgetData); + widgetModels.push(widgetModel); + + // Instantitate the view + var viewClass = widgetClasses[widgetData.type].view; + var viewClassParts = viewClass.split('.'); + var widgetView = new cdb.geo.ui.Widget[viewClassParts[0]][viewClassParts[1]]( + { model: widgetModel } + ); + + $('.js-canvas').append(widgetView.render().el); + }); + + this.map.layers.reset(_.map(data.layers, function(layerData) { + var model; + + if (layerData.type === 'layergroup' || layerData.type === 'namedmap') { + + var layersFromVizJSON = layerData.options.layer_definition && layerData.options.layer_definition.layers || + layerData.options.named_map && layerData.options.named_map.layers; + + var cartoDBLayers = _.map(layersFromVizJSON, function(layer) { + // TODO: This case should be handled by the factory or we + // should add a type to the layers inside of a named_map + if (!layer.type) { + return Layers.create('CartoDB', self, layer); + } + return Layers.create(layer.type, self, layer); + }); + + model = new cdb.geo.CartoDBGroupLayer({}, { + layers: cartoDBLayers + }); + + // TODO: Perhaps this "endpoing" could be part of the "datasource"? + var endpoint = cdb.windshaft.config.MAPS_API_BASE_URL; + var configGenerator = cdb.windshaft.PublicDashboardConfig; + var datasource = data.datasource; + // TODO: We can use something else to differentiate types of "datasource"s + if (datasource.template_name) { + endpoint = [cdb.windshaft.config.MAPS_API_BASE_URL, 'named', datasource.template_name].join('/'); + configGenerator = cdb.windshaft.PrivateDashboardConfig; + } + + var windshaftClient = new cdb.windshaft.Client({ + endpoint: endpoint, + windshaftURLTemplate: datasource.maps_api_template, + userName: datasource.user_name, + statTag: datasource.stat_tag, + forceCors: datasource.force_cors + }); + + var dashboard = new cdb.windshaft.Dashboard({ + client: windshaftClient, + configGenerator: configGenerator, + statTag: datasource.stat_tag, + cartoDBLayerGroup: model, + widgets: widgetModels + }); + + dashboard.createInstance(); + } else { + model = Layers.create(layerData.type || layerData.kind, self, layerData); + } + return model; + })); + + this.overlayModels.reset(data.overlays); + + // if there are no sublayer_options fill it + if (!options.sublayer_options) { + this._setupSublayers(data.layers, options); + } + + this._setLayerOptions(options); + + if (data.slides) { + + this.map.disableKeyboard(); + + function odysseyLoaded() { + self._createSlides([data].concat(data.slides)); + }; + + if (cartodb.odyssey === undefined) { + cdb.config.bind('moduleLoaded:odyssey', odysseyLoaded); + Loader.loadModule('odyssey'); + } else { + odysseyLoaded(); + } + + } + + + _.defer(function() { + self.trigger('done', self, map.layers); + }) + + return this; + + }, + + _addTimeSlider: function() { + var self = this; + var torque = _(this.getLayers()).find(function(layer) { + return layer.model.get('type') === 'torque' && layer.model.get('visible'); + }); + if (torque) { + this.torqueLayer = torque; + // send step events from torque layer + this.torqueLayer.bind('change:time', function(s) { + this.trigger('change:step', this.torqueLayer, this.torqueLayer.getStep()); + }, this); + if (!this.mobile_enabled && this.torqueLayer) { + this.addTimeSlider(this.torqueLayer); + } + } + }, + + // sets the animation step if there is an animation + // returns true if succed + setAnimationStep: function(s, opt) { + if (this.torqueLayer) { + this.torqueLayer.setStep(s, opt); + return true; + } + return false; + }, + + _createSlides: function(slides) { + + function BackboneActions(model) { + var actions = { + set: function() { + var args = arguments; + return O.Action({ + enter: function() { + model.set.apply(model, args); + } + }); + }, + + reset: function() { + var args = arguments; + return O.Action({ + enter: function() { + model.reset.apply(model, args); + } + }); + } + }; + return actions; + } + + function SetStepAction(vis, step) { + return O.Action(function() { + vis.setAnimationStep(step); + }); + } + + function AnimationTrigger(vis, step) { + var t = O.Trigger(); + vis.on('change:step', function (layer, currentStep) { + if (currentStep === step) { + t.trigger(); + } + }); + return t; + } + + function PrevTrigger(seq, step) { + var t = O.Trigger(); + var c = PrevTrigger._callbacks; + if (!c) { + c = PrevTrigger._callbacks = [] + O.Keys().left().then(function() { + for (var i = 0; i < c.length; ++i) { + if (c[i] === seq.current()) { + t.trigger(); + return; + } + } + }); + } + c.push(step); + return t; + } + + function NextTrigger(seq, step) { + var t = O.Trigger(); + var c = NextTrigger._callbacks; + if (!c) { + c = NextTrigger._callbacks = [] + O.Keys().right().then(function() { + for (var i = 0; i < c.length; ++i) { + if (c[i] === seq.current()) { + t.trigger(); + return; + } + } + }); + } + c.push(step); + return t; + } + + function WaitAction(seq, ms) { + return O.Step(O.Sleep(ms), O.Action(function() { + seq.next(); + })); + } + + var self = this; + + var seq = this.sequence = O.Sequential(); + this.slides = O.Story(); + + // transition - debug, remove + //O.Keys().left().then(seq.prev, seq); + //O.Keys().right().then(seq.next, seq); + + this.map.actions = BackboneActions(this.map); + this.map.layers.actions = BackboneActions(this.map.layers); + this.overlayModels.actions = BackboneActions(this.overlayModels) + + function goTo(seq, i) { + return function() { + seq.current(i); + } + } + + for (var i = 0; i < slides.length; ++i) { + var slide = slides[i]; + var states = []; + + var mapChanges = O.Step( + // map movement + this.map.actions.set({ + 'center': typeof slide.center === 'string' ? JSON.parse(slide.center): slide.center, + 'zoom': slide.zoom + }), + // wait a little bit + O.Sleep(350), + // layer change + this.map.layers.actions.reset(_.map(slide.layers, function(layerData) { + return Layers.create(layerData.type || layerData.kind, self, layerData); + })) + ); + + states.push(mapChanges); + + // overlays + states.push(this.overlayModels.actions.reset(slide.overlays)); + + if (slide.transition_options) { + var to = slide.transition_options; + if (to.transition_trigger === 'time') { + states.push(WaitAction(seq, to.time * 1000)); + } else { //default is click + NextTrigger(seq, i).then(seq.next, seq); + PrevTrigger(seq, i).then(seq.prev, seq); + } + } + + this.slides.addState( + seq.step(i), + O.Parallel.apply(window, states) + ); + + } + this.slides.go(0); + }, + + _createOverlays: function(overlays, vis_data, options) { + + // if there's no header overlay, we need to explicitly create the slide controller + if ((options["slides_controller"] || options["slides_controller"] === undefined) && !this.mobile_enabled && !_.find(overlays, function(o) { return o.type === 'header' && o.options.display; })) { + this._addSlideController(vis_data); + } + + _(overlays).each(function(data) { + var type = data.type; + + // We don't render certain overlays if we are in mobile + if (this.mobile_enabled && (type === "zoom" || type === "header" || type === "loader")) return; + + // IE<10 doesn't support the Fullscreen API + if (type === 'fullscreen' && cdb.core.util.browser.ie && cdb.core.util.browser.ie.version <= 10) return; + + // Decide to create or not the custom overlays + if (type === 'image' || type === 'text' || type === 'annotation') { + var isDevice = data.options.device == "mobile" ? true : false; + if (this.mobile !== isDevice) return; + if (!options[type] && options[type] !== undefined) { + return; + } + } + + // We add the header overlay + if (type === 'header') { + var overlay = this._addHeader(data, vis_data); + } else { + var overlay = this.addOverlay(data); + } + + // We show/hide the overlays + if (overlay && (type in options) && options[type] === false) overlay.hide(); + + var opt = data.options; + + if (!this.mobile_enabled) { + + if (type == 'share' && options["shareable"] || type == 'share' && overlay.model.get("display") && options["shareable"] == undefined) overlay.show(); + if (type == 'layer_selector' && options[type] || type == 'layer_selector' && overlay.model.get("display") && options[type] == undefined) overlay.show(); + if (type == 'fullscreen' && options[type] || type == 'fullscreen' && overlay.model.get("display") && options[type] == undefined) overlay.show(); + if (type == 'search' && options[type] || type == 'search' && opt.display && options[type] == undefined) overlay.show(); + + if (type === 'header') { + + var m = overlay.model; + + if (options.title !== undefined) { + m.set("show_title", options.title); + } + + if (options.description !== undefined) { + m.set("show_description", options.description); + } + + if (m.get('show_title') || m.get('show_description')) { + $(".cartodb-map-wrapper").addClass("with_header"); + } + + overlay.render(); + } + } + + + }, this); + + }, + + _addSlideController: function(data) { + + if (data.slides && data.slides.length > 0) { + + var transitions = [data.transition_options].concat(_.pluck(data.slides, "transition_options")); + + return this.addOverlay({ + type: 'slides_controller', + transitions: transitions + }); + } + + }, + + _addHeader: function(data, vis_data) { + + var transitions = [vis_data.transition_options].concat(_.pluck(vis_data.slides, "transition_options")) + + return this.addOverlay({ + type: 'header', + options: data.options, + transitions: transitions + }); + + }, + + _addMobile: function(data, options) { + + var layers; + var layer = data.layers[1]; + + if (this.mobile_enabled) { + + if (options && options.legends === undefined) { + options.legends = this.legends ? true : false; + } + + if (layer.options && layer.options.layer_definition) { + layers = layer.options.layer_definition.layers; + } else if (layer.options && layer.options.named_map && layer.options.named_map.layers) { + layers = layer.options.named_map.layers; + } + + var transitions = [data.transition_options].concat(_.pluck(data.slides, "transition_options")); + + this.mobileOverlay = this.addOverlay({ + type: 'mobile', + layers: layers, + slides: data.slides, + transitions:transitions, + overlays: data.overlays, + options: options, + torqueLayer: this.torqueLayer + }); + } + }, + + _createLegendView: function(layer, layerView) { + if (layer.legend) { + layer.legend.data = layer.legend.items; + var legend = layer.legend; + + if ((legend.items && legend.items.length) || legend.template) { + var legendAttrs = _.extend(layer.legend, { + visible: layer.visible + }); + var legendModel = new cdb.geo.ui.LegendModel(legendAttrs); + var legendView = new cdb.geo.ui.Legend({ model: legendModel }); + layerView.bind('change:visibility', function(layer, hidden) { + legendView[hidden ? 'hide': 'show'](); + }); + layerView.legend = legendModel; + return legendView; + } + } + return null; + }, + + createLegendView: function(layers) { + var legends = []; + var self = this; + for (var i = layers.length - 1; i >= 0; --i) { + var cid = layers.at(i).cid; + var layer = layers.at(i).attributes; + if (layer.visible) { + var layerView = this.mapView.getLayerByCid(cid); + if (layerView) { + var layerView = this.mapView.getLayerByCid(cid); + legends.push(this._createLayerLegendView(layer, layerView)); + } + } + } + return _.flatten(legends); + }, + + _createLayerLegendView: function(layer, layerView) { + var self = this; + var legends = []; + if (layer.options && layer.options.layer_definition) { + var sublayers = layer.options.layer_definition.layers; + _(sublayers).each(function(sub, i) { + legends.push(self._createLegendView(sub, layerView.getSubLayer(i))); + }); + } else if(layer.options && layer.options.named_map && layer.options.named_map.layers) { + var sublayers = layer.options.named_map.layers; + _(sublayers).each(function(sub, i) { + legends.push(self._createLegendView(sub, layerView.getSubLayer(i))); + }); + } else { + legends.push(this._createLegendView(layer, layerView)) + } + return _.compact(legends).reverse(); + }, + + addOverlay: function(overlay) { + + overlay.map = this.map; + + var v = Overlay.create(overlay.type, this, overlay); + + if (v) { + // Save tiles loader view for later + if (overlay.type == "loader") { + this.loader = v; + } + + this.mapView.addOverlay(v); + + this.overlays.push(v); + + v.bind('clean', function() { + for(var i in this.overlays) { + var o = this.overlays[i]; + if (v.cid === o.cid) { + this.overlays.splice(i, 1) + return; + } + } + }, this); + } + return v; + }, + + // change vizjson based on options + _applyOptions: function(vizjson, opt) { + opt = opt || {}; + opt = _.defaults(opt, { + tiles_loader: true, + loaderControl: true, + infowindow: true, + tooltip: true, + time_slider: true + }); + vizjson.overlays = vizjson.overlays || []; + vizjson.layers = vizjson.layers || []; + + function search_overlay(name) { + if (!vizjson.overlays) return null; + for(var i = 0; i < vizjson.overlays.length; ++i) { + if (vizjson.overlays[i].type === name) { + return vizjson.overlays[i]; + } + } + } + + function remove_overlay(name) { + if (!vizjson.overlays) return; + for(var i = 0; i < vizjson.overlays.length; ++i) { + if (vizjson.overlays[i].type === name) { + vizjson.overlays.splice(i, 1); + return; + } + } + } + + this.infowindow = opt.infowindow; + this.tooltip = opt.tooltip; + + if (opt.https) { + this.https = true; + } + + if (opt.gmaps_base_type) { + this.gmaps_base_type = opt.gmaps_base_type; + } + + if (opt.gmaps_style) { + this.gmaps_style = opt.gmaps_style; + } + + this.mobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + this.mobile_enabled = (opt.mobile_layout && this.mobile) || opt.force_mobile; + + if (opt.force_mobile === false || opt.force_mobile === "false") this.mobile_enabled = false; + + if (!opt.title) { + vizjson.title = null; + } + + if (!opt.description) { + vizjson.description = null; + } + + if (!opt.tiles_loader) { + remove_overlay('loader'); + } + + if (!opt.loaderControl) { + remove_overlay('loader'); + } + + if (opt.searchControl !== undefined) { + opt.search = opt.searchControl; + } + + if (!this.mobile_enabled && opt.search) { + if (!search_overlay('search')) { + vizjson.overlays.push({ + type: "search", + order: 3 + }); + } + } + + if ( (opt.title && vizjson.title) || (opt.description && vizjson.description) ) { + + if (!search_overlay('header')) { + vizjson.overlays.unshift({ + type: "header", + order: 1, + shareable: opt.shareable ? true: false, + url: vizjson.url, + options: { + extra: { + title: vizjson.title, + description: vizjson.description, + show_title: opt.title, + show_description: opt.description + } + } + }); + } + } + + + if (opt.layer_selector) { + if (!search_overlay('layer_selector')) { + vizjson.overlays.push({ + type: "layer_selector" + }); + } + } + + if (opt.shareable && !this.mobile_enabled) { + if (!search_overlay('share')) { + vizjson.overlays.push({ + type: "share", + order: 2, + url: vizjson.url + }); + } + } + + // We remove certain overlays in mobile devices + if (this.mobile_enabled) { + remove_overlay('logo'); + remove_overlay('share'); + } + + if (this.mobile || ((opt.zoomControl !== undefined) && (!opt.zoomControl)) ){ + remove_overlay('zoom'); + } + + if (this.mobile || ((opt.search !== undefined) && (!opt.search)) ){ + remove_overlay('search'); + } + + // if bounds are present zoom and center will not taken into account + var zoom = parseInt(opt.zoom); + if (!isNaN(zoom)) { + vizjson.zoom = zoom; + vizjson.bounds = null; + } + + // Center coordinates? + var center_lat = parseFloat(opt.center_lat); + var center_lon = parseFloat(opt.center_lon); + if ( !isNaN(center_lat) && !isNaN(center_lon) ) { + vizjson.center = [center_lat, center_lon]; + vizjson.bounds = null; + } + + // Center object + if (opt.center !== undefined) { + vizjson.center = opt.center; + vizjson.bounds = null; + } + + // Bounds? + var sw_lat = parseFloat(opt.sw_lat); + var sw_lon = parseFloat(opt.sw_lon); + var ne_lat = parseFloat(opt.ne_lat); + var ne_lon = parseFloat(opt.ne_lon); + + if ( !isNaN(sw_lat) && !isNaN(sw_lon) && !isNaN(ne_lat) && !isNaN(ne_lon) ) { + vizjson.bounds = [ + [ sw_lat, sw_lon ], + [ ne_lat, ne_lon ] + ]; + } + + if (vizjson.layers.length > 1) { + var token = opt.auth_token; + function _applyLayerOptions(layers) { + for(var i = 1; i < layers.length; ++i) { + var o = layers[i].options; + o.no_cdn = opt.no_cdn; + o.force_cors = opt.force_cors; + if(token) { + o.auth_token = token; + } + } + } + _applyLayerOptions(vizjson.layers); + if (vizjson.slides) { + for(var i = 0; i < vizjson.slides.length; ++i) { + _applyLayerOptions(vizjson.slides[i].layers); + } + } + } + + }, + + // Set map top position taking into account header height + setMapPosition: function() { }, + + createLayer: function(layerData, opts) { + var layerModel = Layers.create(layerData.type || layerData.kind, this, layerData); + return this.mapView.createLayer(layerModel); + }, + + _getSqlApi: function(attrs) { + attrs = attrs || {}; + var port = attrs.sql_api_port + var domain = attrs.sql_api_domain + (port ? ':' + port: '') + var protocol = attrs.sql_api_protocol; + var version = 'v1'; + if (domain.indexOf('cartodb.com') !== -1) { + protocol = 'http'; + domain = "cartodb.com"; + version = 'v2'; + } + + var sql = new cartodb.SQL({ + user: attrs.user_name, + protocol: protocol, + host: domain, + version: version + }); + + return sql; + }, + + addTooltip: function(layerView) { + + var layers = layerView.model && layerView.model.layers || []; + + for(var i = 0; i < layers.length; ++i) { + var layerModel = layers.at(i); + var t = layerModel.getTooltipData(); + if (t) { + if (!layerView.tooltip) { + var tooltip = new cdb.geo.ui.Tooltip({ + mapView: this.mapView, + layer: layerView, + template: t.template, + position: 'bottom|right', + vertical_offset: 10, + horizontal_offset: 4, + fields: t.fields, + omit_columns: ['cartodb_id'] + }); + layerView.tooltip = tooltip; + this.mapView.addOverlay(tooltip); + layerView.bind('remove', function() { + this.tooltip.clean(); + }); + } + } + } + + if (layerView.tooltip) { + layerView.bind("featureOver", function(e, latlng, pos, data, layer) { + var t = layers.at(layer).getTooltipData(); + if (t) { + layerView.tooltip.setTemplate(t.template); + layerView.tooltip.setFields(t.fields); + layerView.tooltip.setAlternativeNames(t.alternative_names); + layerView.tooltip.enable(); + } else { + layerView.tooltip.disable(); + } + }); + } + }, + + addInfowindow: function(layerView) { + + var mapView = this.mapView; + var infowindow = null; + var layers = layerView.model && layerView.model.layers || []; + + for(var i = 0; i < layers.length; ++i) { + var layerModel = layers.at(i); + if (layerModel.getInfowindowData()) { + if(!infowindow) { + infowindow = Overlay.create('infowindow', this, layerModel.getInfowindowData(), true); + mapView.addInfowindow(infowindow); + } + } + } + + if(!infowindow) { + return; + } + + infowindow.bind('close', function() { + // when infowindow is closed remove all the filters + // for tooltips + for(var i = 0; i < layers; ++i) { + var t = layerView.tooltip; + if (t) { + t.setFilter(null); + } + } + }) + + // if the layer has no infowindow just pass the interaction + // data to the infowindow + layerView.bind('featureClick', function(e, latlng, pos, data, layer) { + + var infowindowFields = layers.at(layer).getInfowindowData(); + if (!infowindowFields) return; + var cartodb_id = data.cartodb_id; + + layerView.model.fetchAttributes(layer, cartodb_id, function(attributes) { + + // Old viz.json doesn't contain width and maxHeight properties + // and we have to get the default values if there are not defined. + var extra = _.defaults( + { + offset: infowindowFields.offset, + width: infowindowFields.width, + maxHeight: infowindowFields.maxHeight + }, + cdb.geo.ui.InfowindowModel.prototype.defaults + ); + + infowindow.model.set({ + 'fields': infowindowFields.fields, + 'template': infowindowFields.template, + 'template_type': infowindowFields.template_type, + 'alternative_names': infowindowFields.alternative_names, + 'sanitizeTemplate': infowindowFields.sanitizeTemplate, + 'offset': extra.offset, + 'width': extra.width, + 'maxHeight': extra.maxHeight + }); + + if (attributes) { + infowindow.model.updateContent(attributes); + infowindow.adjustPan(); + } else { + infowindow.setError(); + } + }); + + // Show infowindow with loading state + infowindow + .setLatLng(latlng) + .setLoading() + .showInfowindow(); + + if (layerView.tooltip) { + layerView.tooltip.setFilter(function(feature) { + return feature.cartodb_id !== cartodb_id; + }).hide(); + } + }); + + var hovers = []; + + layerView.bind('mouseover', function() { + mapView.setCursor('pointer'); + }); + + layerView.bind('mouseout', function(m, layer) { + mapView.setCursor('auto'); + }); + + layerView.infowindow = infowindow.model; + }, + + _addLoading: function (layerView) { + if (layerView) { + var self = this; + + var loadingTiles = function() { + self.loadingTiles(); + }; + + var loadTiles = function() { + self.loadTiles(); + }; + + layerView.bind('loading', loadingTiles); + layerView.bind('load', loadTiles); + } + }, + + + loadingTiles: function() { + + if (this.mobileOverlay) { + this.mobileOverlay.loadingTiles(); + } + + if (this.loader) { + this.loader.show() + } + if(this.layersLoading === 0) { + this.trigger('loading'); + } + this.layersLoading++; + }, + + loadTiles: function() { + + if (this.mobileOverlay) { + this.mobileOverlay.loadTiles(); + } + + if (this.loader) { + this.loader.hide(); + } + this.layersLoading--; + // check less than 0 because loading event sometimes is + // thrown before visualization creation + if(this.layersLoading <= 0) { + this.layersLoading = 0; + this.trigger('load'); + } + }, + + throwError: function(msg, lyr) { + cdb.log.error(msg); + var self = this; + _.defer(function() { + self.trigger('error', msg, lyr); + }); + }, + + error: function(fn) { + return this.bind('error', fn); + }, + + done: function(fn) { + return this.bind('done', fn); + }, + + // public methods + // + + // get the native map used behind the scenes + getNativeMap: function() { + return this.mapView.getNativeMap(); + }, + + // returns an array of layers + getLayers: function() { + var self = this; + return _.compact(this.map.layers.map(function(layer) { + return self.mapView.getLayerByCid(layer.cid); + })); + }, + + getOverlays: function() { + return this.overlays; + }, + + getOverlay: function(type) { + return _(this.overlays).find(function(v) { + return v.type == type; + }); + }, + + getOverlaysByType: function(type) { + return _(this.overlays).filter(function(v) { + return v.type == type; + }); + }, + + _onResize: function() { + + $(window).unbind('resize', this._onResize); + + var self = this; + + self.mapView.invalidateSize(); + + // This timeout is necessary due to GMaps needs time + // to load tiles and recalculate its bounds :S + setTimeout(function() { + + var c = self.mapConfig; + + if (c.view_bounds_sw) { + + self.mapView.map.setBounds([ + c.view_bounds_sw, + c.view_bounds_ne + ]); + + } else { + + self.mapView.map.set({ + center: c.center, + zoom: c.zoom + }); + + } + }, 150); + } + +}, { + + /** + * adds an infowindow to the map controlled by layer events. + * it enables interaction and overrides the layer interacivity + * ``fields`` array of column names + * ``map`` native map object, leaflet of gmaps + * ``layer`` cartodb layer (or sublayer) + */ + addInfowindow: function(map, layer, fields, opts) { + var options = _.defaults(opts || {}, { + infowindowTemplate: cdb.vis.INFOWINDOW_TEMPLATE.light, + templateType: 'mustache', + triggerEvent: 'featureClick', + templateName: 'light', + extraFields: [], + cursorInteraction: true + }); + + if(!map) throw new Error('map is not valid'); + if(!layer) throw new Error('layer is not valid'); + if(!fields && fields.length === undefined ) throw new Error('fields should be a list of strings'); + + var f = []; + fields = fields.concat(options.extraFields); + for(var i = 0; i < fields.length; ++i) { + f.push({ name: fields, order: i}); + } + + var infowindowModel = new cdb.geo.ui.InfowindowModel({ + fields: f, + template_name: options.templateName + }); + + var infowindow = new cdb.geo.ui.Infowindow({ + model: infowindowModel, + mapView: map.viz.mapView, + template: new cdb.core.Template({ + template: options.infowindowTemplate, + type: options.templateType + }).asFunction() + }); + + map.viz.mapView.addInfowindow(infowindow); + // try to change interactivity, it the layer is a named map + // it's inmutable so it'a assumed the interactivity already has + // the fields it needs + try { + layer.setInteractivity(fields); + } catch(e) { + } + layer.setInteraction(true); + + layer.bind(options.triggerEvent, function(e, latlng, pos, data, layer) { + var render_fields = []; + var d; + for (var f = 0; f < fields.length; ++f) { + var field = fields[f]; + if (d = data[field]) { + render_fields.push({ + title: field, + value: d, + index: 0 + }); + } + } + + infowindow.model.set({ + content: { + fields: render_fields, + data: data + } + }); + + infowindow + .setLatLng(latlng) + .showInfowindow(); + infowindow.adjustPan(); + }, infowindow); + + // remove the callback on clean + infowindow.bind('clean', function() { + layer.unbind(options.triggerEvent, null, infowindow); + }); + + if(options.cursorInteraction) { + cdb.vis.Vis.addCursorInteraction(map, layer); + } + + return infowindow; + + }, + + addCursorInteraction: function(map, layer) { + var mapView = map.viz.mapView; + layer.bind('mouseover', function() { + mapView.setCursor('pointer'); + }); + + layer.bind('mouseout', function(m, layer) { + mapView.setCursor('auto'); + }); + }, + + removeCursorInteraction: function(map, layer) { + var mapView = map.viz.mapView; + layer.unbind(null, null, mapView); + } + +}); + +cdb.vis.INFOWINDOW_TEMPLATE = { + light: [ + '
          ', + 'x', + '
          ', + '
          ', + '{{#content.fields}}', + '{{#title}}

          {{title}}

          {{/title}}', + '{{#value}}', + '

          {{{ value }}}

          ', + '{{/value}}', + '{{^value}}', + '

          null

          ', + '{{/value}}', + '{{/content.fields}}', + '
          ', + '
          ', + '
          ', + '
          ' + ].join('') +}; + +cdb.vis.Vis = Vis; + +})(); + +(function() { + + Queue = function() { + + // callback storage + this._methods = []; + + // reference to the response + this._response = null; + + // all queues start off unflushed + this._flushed = false; + + }; + + Queue.prototype = { + + // adds callbacks to the queue + add: function(fn) { + + // if the queue had been flushed, return immediately + if (this._flushed) { + + // otherwise push it on the queue + fn(this._response); + + } else { + this._methods.push(fn); + } + + }, + + flush: function(resp) { + + // flush only ever happens once + if (this._flushed) { + return; + } + + // store the response for subsequent calls after flush() + this._response = resp; + + // mark that it's been flushed + this._flushed = true; + + // shift 'em out and call 'em back + while (this._methods[0]) { + this._methods.shift()(resp); + } + + } + + }; + + StaticImage = function() { + + MapBase.call(this, this); + + this.imageOptions = {}; + + this.error = null; + + this.supported_formats = ["png", "jpg"]; + + this.defaults = { + basemap_url_template: "http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", + basemap_subdomains: ["a", "b", "c"], + format: "png", + zoom: 10, + center: [0, 0], + size: [320, 240], + tiler_port: 80, + tiler_domain: "cartodb.com" + }; + + }; + + StaticImage.prototype = _.extend({}, MapBase.prototype, { + + load: function(vizjson, options) { + + _.bindAll(this, "_onVisLoaded"); + + this.queue = new Queue; + + this.no_cdn = options.no_cdn; + + this.userOptions = options; + + options = _.defaults({ vizjson: vizjson, temp_id: "s" + this._getUUID() }, this.defaults); + + this.imageOptions = options; + + cdb.core.Loader.get(vizjson, this._onVisLoaded); + + return this; + + }, + + loadLayerDefinition: function(layerDefinition, options) { + + var self = this; + + this.queue = new Queue; + + if (!layerDefinition.user_name) { + cartodb.log.error("Please, specify the username"); + return; + } + + this.userOptions = options; + + this.options.api_key = layerDefinition.api_key; + this.options.user_name = layerDefinition.user_name; + this.options.tiler_protocol = layerDefinition.tiler_protocol; + this.options.tiler_domain = layerDefinition.tiler_domain; + this.options.tiler_port = layerDefinition.tiler_port; + this.options.maps_api_template = layerDefinition.maps_api_template; + this.endPoint = "/api/v1/map"; + + if (!this.options.maps_api_template) { + this._buildMapsApiTemplate(this.options); + } + + this.options.layers = layerDefinition; + + this._requestLayerGroupID(); + + }, + + _onVisLoaded: function(data) { + + if (data) { + + var layerDefinition; + var baseLayer = data.layers[0]; + var dataLayer = this._getDataLayer(data.layers); + + if (dataLayer.options) { + this.options.user_name = dataLayer.options.user_name; + } + + // keep this for backward compatibility with tiler_* variables + if (!dataLayer.options.maps_api_template) { + this._setupTilerConfiguration(dataLayer.options.tiler_protocol, dataLayer.options.tiler_domain, dataLayer.options.tiler_port); + } else { + this.options.maps_api_template = dataLayer.options.maps_api_template; + } + + this.auth_tokens = data.auth_tokens; + this.endPoint = "/api/v1/map"; + + var bbox = []; + var bounds = data.bounds; + + if (bounds) { + bbox.push([bounds[0][1], bounds[0][0]]); + bbox.push([bounds[1][1], bounds[1][0]]); + } + + this.imageOptions.zoom = data.zoom; + this.imageOptions.center = JSON.parse(data.center); + this.imageOptions.bbox = bbox; + this.imageOptions.bounds = data.bounds; + + if (baseLayer && baseLayer.options) { + this.imageOptions.basemap = baseLayer; + } + + /* If the vizjson contains a named map and a torque layer with a named map, + ignore the torque layer */ + var ignoreTorqueLayer = false; + var namedMap = this._getLayerByType(data.layers, "namedmap"); + + if (namedMap) { + var torque = this._getLayerByType(data.layers, "torque"); + + if (torque && torque.options && torque.options.named_map) { + + if (torque.options.named_map.name === namedMap.options.named_map.name) { + ignoreTorqueLayer = true; + } + } + } + + var layers = []; + var basemap = this._getBasemapLayer(); + + if (basemap) { + layers.push(basemap); + } + + var labelsLayer; + for (var i = 1; i < data.layers.length; i++) { + var layer = data.layers[i]; + + if (layer.type === "torque" && !ignoreTorqueLayer) { + layers.push(this._getTorqueLayerDefinition(layer)); + } else if (layer.type === "namedmap") { + layers.push(this._getNamedmapLayerDefinition(layer)); + } else if (layer.type === "tiled") { + labelsLayer = this._getHTTPLayer(layer); + } else if (layer.type !== "torque" && layer.type !== "namedmap") { + var ll = this._getLayergroupLayerDefinition(layer); + + for (var j = 0; j < ll.length; j++) { + layers.push(ll[j]); + } + } + } + + // If there's a second `tiled` layer, it's a layer with labels and + // it needs to be on top of all other layers + if (labelsLayer) { + layers.push(labelsLayer); + } + + this.options.layers = { layers: layers }; + this._requestLayerGroupID(); + } + }, + + _getDataLayer: function(layers) { + return this._getLayerByType(layers, "namedmap") || + this._getLayerByType(layers, "layergroup") || + this._getLayerByType(layers, "torque"); + }, + + visibleLayers: function() { + // Overwrites the layer_definition method. + // We return all the layers, since we have filtered them before + return this.options.layers.layers; + }, + + _getLayerByType: function(layers, type) { + return _.find(layers, function(layer) { return layer.type === type; }); + }, + + _setupTilerConfiguration: function(protocol, domain, port) { + + this.options.tiler_domain = domain; + this.options.tiler_protocol = protocol; + this.options.tiler_port = port; + + this._buildMapsApiTemplate(this.options); + + }, + + toJSON: function(){ + return this.options.layers; + }, + + _requestLayerGroupID: function() { + + var self = this; + + this.createMap(function(data, error) { + + if (error) { + self.error = error; + } + + if (data) { + self.imageOptions.layergroupid = data.layergroupid; + self.cdn_url = data.cdn_url; + } + + self.queue.flush(this); + + }); + + }, + + _getDefaultBasemapLayer: function() { + + return { + type: "http", + options: { + urlTemplate: this.defaults.basemap_url_template, + subdomains: this.defaults.basemap_subdomains + } + }; + + }, + + _getHTTPLayer: function(basemap) { + + var urlTemplate = basemap.options.urlTemplate; + + if (!urlTemplate) { + return null; + } + + return { + type: "http", + options: { + urlTemplate: urlTemplate, + subdomains: basemap.options.subdomains || this.defaults.basemap_subdomains + } + }; + + }, + + _getPlainBasemapLayer: function(color) { + + return { + type: "plain", + options: { + color: color + } + }; + + }, + + _getBasemapLayer: function() { + + var basemap = this.userOptions.basemap || this.imageOptions.basemap; + + if (basemap) { + + // TODO: refactor this + var type = basemap.type.toLowerCase(); + + if (basemap.options && basemap.options.type) { + type = basemap.options.type.toLowerCase(); + } + + if (type === "plain") { + return this._getPlainBasemapLayer(basemap.options.color); + } else { + return this._getHTTPLayer(basemap); + } + + } + + return this._getDefaultBasemapLayer(); + + }, + + _getTorqueLayerDefinition: function(layer_definition) { + + if (layer_definition.options.named_map) { // If the layer contains a named map inside, use it instead + return this._getNamedmapLayerDefinition(layer_definition); + } + + var layerDefinition = new LayerDefinition(layer_definition, layer_definition.options); + + var query = layerDefinition.options.query || "SELECT * FROM " + layerDefinition.options.table_name; + var cartocss = layer_definition.options.tile_style; + + return { + type: "torque", + options: { + step: this.userOptions.step || 0, + sql: query, + cartocss: cartocss + } + }; + + }, + + _getLayergroupLayerDefinition: function(layer) { + + var options = layer.options; + + options.layer_definition.layers = this._getVisibleLayers(options.layer_definition.layers); + + var layerDefinition = new LayerDefinition(options.layer_definition, options); + + return layerDefinition.toJSON().layers; + + }, + + _getNamedmapLayerDefinition: function(layer) { + + var options = layer.options; + + var layerDefinition = new NamedMap(options.named_map, options); + + var options = { + name: layerDefinition.named_map.name + }; + + if (this.auth_tokens && this.auth_tokens.length > 0) { + options.auth_tokens = this.auth_tokens; + } + + return { + type: "named", + options: options + } + + }, + + _getVisibleLayers: function(layers) { + return _.filter(layers, function(layer) { return layer.visible; }); + }, + + _getUrl: function() { + + var username = this.options.user_name; + var bbox = this.imageOptions.bbox; + var layergroupid = this.imageOptions.layergroupid; + var zoom = this.imageOptions.zoom || this.defaults.zoom; + var center = this.imageOptions.center || this.defaults.center; + var size = this.imageOptions.size || this.defaults.size; + var format = this.imageOptions.format || this.defaults.format; + + var lat = center[0]; + var lon = center[1]; + + var width = size[0]; + var height = size[1]; + + var subhost = this.isHttps() ? null : "a"; + + var url = this._host(subhost) + this.endPoint; + + if (bbox && bbox.length && !this.userOptions.override_bbox) { + return [url, "static/bbox" , layergroupid, bbox.join(","), width, height + "." + format].join("/"); + } else { + return [url, "static/center" , layergroupid, zoom, lat, lon, width, height + "." + format].join("/"); + } + + }, + + // Generates a random string + _getUUID: function() { + var S4 = function() { + return (((1+Math.random())*0x10000)|0).toString(16).substring(1); + }; + return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); + }, + + /* Setters */ + _set: function(name, value) { + + var self = this; + + this.queue.add(function() { + self.imageOptions[name] = value; + }); + + return this; + + }, + + zoom: function(zoom) { + return this._set("zoom", zoom); + }, + + bbox: function(bbox) { + return this._set("bbox", bbox); + }, + + center: function(center) { + this._set("bbox", null); + return this._set("center", center); + }, + + format: function(format) { + return this._set("format", _.include(this.supported_formats, format) ? format : this.defaults.format); + }, + + size: function(width, height) { + return this._set("size", [width, height === undefined ? width : height]); + }, + + /* Methods */ + + /* Image.into(HTMLImageElement) + inserts the image in the HTMLImageElement specified */ + into: function(img) { + + var self = this; + + if (!(img instanceof HTMLImageElement)) { + cartodb.log.error("img should be an image"); + return; + } + + this.imageOptions.size = [img.width, img.height]; + + this.queue.add(function(response) { + img.src = self._getUrl(); + }); + + }, + + /* Image.getUrl(callback(err, url)) + gets the url for the image, err is null is there was no error */ + + getUrl: function(callback) { + + var self = this; + + this.queue.add(function() { + if (callback) { + callback(self.error, self._getUrl()); + } + }); + + }, + + /* Image.write(attributes) + adds a img tag in the same place script is executed */ + + write: function(attributes) { + + var self = this; + + this.imageOptions.attributes = attributes; + + if (attributes && attributes.src) { + document.write(''); + } else { + document.write(''); + } + + this.queue.add(function() { + + var element = document.getElementById(self.imageOptions.temp_id); + + element.src = self._getUrl(); + element.removeAttribute("temp_id"); + + var attributes = self.imageOptions.attributes; + + if (attributes && attributes.class) { element.setAttribute("class", attributes.class); } + if (attributes && attributes.id) { element.setAttribute("id", attributes.id); } + + }); + + return this; + } + + }) + + cdb.Image = function(data, options) { + + if (!options) options = {}; + + var image = new StaticImage(); + + if (typeof data === 'string') { + image.load(data, options); + } else { + image.loadLayerDefinition(data, options); + } + + return image; + + }; + +})(); + +(function() { + +cdb.vis.Overlay.register('logo', function(data, vis) { + +}); + +cdb.vis.Overlay.register('slides_controller', function(data, vis) { + + var slides_controller = new cdb.geo.ui.SlidesController({ + transitions: data.transitions, + visualization: vis + }); + + return slides_controller.render(); + +}); + +cdb.vis.Overlay.register('mobile', function(data, vis) { + + var template = cdb.core.Template.compile( + data.template || '\ +
          \ +
          \ +
          \ + \ + \ +
          \ +
          \ +
          \ +
          \ +
          \ +
            \ +
            \ +
            \ +
            \ + \ +
            \ + ', + data.templateType || 'mustache' + ); + + var mobile = new cdb.geo.ui.Mobile({ + template: template, + mapView: vis.mapView, + overlays: data.overlays, + transitions: data.transitions, + slides_data: data.slides, + visualization: vis, + layerView: data.layerView, + visibility_options: data.options, + torqueLayer: data.torqueLayer, + map: data.map + }); + + return mobile.render(); +}); + +cdb.vis.Overlay.register('image', function(data, vis) { + + var options = data.options; + + var template = cdb.core.Template.compile( + data.template || '\ +
            \ +
            {{{ content }}}
            \ +
            ', + data.templateType || 'mustache' + ); + + var widget = new cdb.geo.ui.Image({ + model: new cdb.core.Model(options), + template: template + }); + + return widget.render(); + +}); + +cdb.vis.Overlay.register('text', function(data, vis) { + + var options = data.options; + + var template = cdb.core.Template.compile( + data.template || '\ +
            \ +
            {{{ text }}}
            \ +
            ', + data.templateType || 'mustache' + ); + + var widget = new cdb.geo.ui.Text({ + model: new cdb.core.Model(options), + template: template, + className: "cartodb-overlay overlay-text " + options.device + }); + + return widget.render(); + +}); + +cdb.vis.Overlay.register('annotation', function(data, vis) { + + var options = data.options; + + var template = cdb.core.Template.compile( + data.template || '\ +
            \ +
            {{{ text }}}
            \ +
            \ +
            ', + data.templateType || 'mustache' + ); + + var options = data.options; + + var widget = new cdb.geo.ui.Annotation({ + className: "cartodb-overlay overlay-annotation " + options.device, + template: template, + mapView: vis.mapView, + device: options.device, + text: options.extra.rendered_text, + minZoom: options.style["min-zoom"], + maxZoom: options.style["max-zoom"], + latlng: options.extra.latlng, + style: options.style + }); + + return widget.render(); + +}); + + +cdb.vis.Overlay.register('zoom_info', function(data, vis) { + //console.log("placeholder for the zoom_info overlay"); +}); + +cdb.vis.Overlay.register('header', function(data, vis) { + + var options = data.options; + + var template = cdb.core.Template.compile( + data.template || '\ +
            \ +
            {{{ title }}}
            \ +
            {{{ description }}}
            \ +
            ', + data.templateType || 'mustache' + ); + + var widget = new cdb.geo.ui.Header({ + model: new cdb.core.Model(options), + transitions: data.transitions, + slides: vis.slides, + template: template + }); + + return widget.render(); + +}); + +// map zoom control +cdb.vis.Overlay.register('zoom', function(data, vis) { + + if(!data.template) { + vis.trigger('error', 'zoom template is empty') + return; + } + + var zoom = new cdb.geo.ui.Zoom({ + model: data.map, + template: cdb.core.Template.compile(data.template) + }); + + return zoom.render(); + +}); + +// Tiles loader +cdb.vis.Overlay.register('loader', function(data) { + + var tilesLoader = new cdb.geo.ui.TilesLoader({ + template: cdb.core.Template.compile(data.template) + }); + + return tilesLoader.render(); +}); + +cdb.vis.Overlay.register('time_slider', function(data, viz) { + var slider = new cdb.geo.ui.TimeSlider(data); + return slider.render(); +}); + + +// Header to show informtion (title and description) +cdb.vis.Overlay.register('_header', function(data, vis) { + var MAX_SHORT_DESCRIPTION_LENGTH = 100; + + // Add the complete url for facebook and twitter + if (location.href) { + data.share_url = encodeURIComponent(location.href); + } else { + data.share_url = data.url; + } + + var template = cdb.core.Template.compile( + data.template || "\ + {{#title}}\ +

            \ + {{#url}}\ + {{title}}\ + {{/url}}\ + {{^url}}\ + {{title}}\ + {{/url}}\ +

            \ + {{/title}}\ + {{#description}}

            {{{description}}}

            {{/description}}\ + {{#mobile_shareable}}\ + \ + {{/mobile_shareable}}\ + ", + data.templateType || 'mustache' + ); + + function truncate(s, length) { + return s.substr(0, length-1) + (s.length > length ? '…' : ''); + } + + var title = data.map.get('title'); + var description = data.map.get('description'); + + var facebook_title = title + ": " + description; + var twitter_title; + + if (title && description) { + twitter_title = truncate(title + ": " + description, 112) + " %23map " + } else if (title) { + twitter_title = truncate(title, 112) + " %23map" + } else if (description){ + twitter_title = truncate(description, 112) + " %23map" + } else { + twitter_title = "%23map" + } + + var shareable = (data.shareable == "false" || !data.shareable) ? null : data.shareable; + var mobile_shareable = shareable; + + mobile_shareable = mobile_shareable && (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); + + var header = new cdb.geo.ui.Header({ + title: title, + description: description, + facebook_title: facebook_title, + twitter_title: twitter_title, + url: data.url, + share_url: data.share_url, + mobile_shareable: mobile_shareable, + shareable: shareable && !mobile_shareable, + template: template + }); + + return header.render(); +}); + +// infowindow +cdb.vis.Overlay.register('infowindow', function(data, vis) { + + if (_.size(data.fields) == 0) { + return null; + } + + var infowindowModel = new cdb.geo.ui.InfowindowModel({ + template: data.template, + template_type: data.templateType, + alternative_names: data.alternative_names, + fields: data.fields, + template_name: data.template_name + }); + + var infowindow = new cdb.geo.ui.Infowindow({ + model: infowindowModel, + mapView: vis.mapView, + template: data.template + }); + + return infowindow; +}); + + +// layer_selector +cdb.vis.Overlay.register('layer_selector', function(data, vis) { + + var options = data.options; + //if (!options.display) return; + + var template = cdb.core.Template.compile( + data.template || '\ + Visible layers
            \ + ', + data.templateType || 'underscore' + ); + + var dropdown_template = cdb.core.Template.compile( + data.template || '\ +
              \ + ', + data.templateType || 'underscore' + ); + + var layerSelector = new cdb.geo.ui.LayerSelector({ + model: new cdb.core.Model(options), + mapView: vis.mapView, + template: template, + dropdown_template: dropdown_template, + layer_names: data.layer_names + }); + + var timeSlider = vis.timeSlider; + if (timeSlider) { + layerSelector.bind('change:visible', function(visible, order, layer) { + if (layer.get('type') === 'torque') { + timeSlider[visible ? 'show': 'hide'](); + } + }); + } + if (vis.legends) { + + layerSelector.bind('change:visible', function(visible, order, layer) { + + + if (layer.get('type') === 'layergroup' || layer.get('type') === 'torque') { + + var legend = vis.legends && vis.legends.getLegendByIndex(order); + + if (legend) { + legend[visible ? 'show': 'hide'](); + } + + } + + }); + } + + return layerSelector.render(); + +}); + +// fullscreen +cdb.vis.Overlay.register('fullscreen', function(data, vis) { + + var options = data.options; + + options.allowWheelOnFullscreen = false; + + var template = cdb.core.Template.compile( + data.template || '', + data.templateType || 'mustache' + ); + + var fullscreen = new cdb.ui.common.FullScreen({ + doc: "#map > div", + model: new cdb.core.Model(options), + mapView: vis.mapView, + template: template + }); + + return fullscreen.render(); + +}); + +// share content +cdb.vis.Overlay.register('share', function(data, vis) { + + var options = data.options; + + var template = cdb.core.Template.compile( + data.template || '', + data.templateType || 'mustache' + ); + + var widget = new cdb.geo.ui.Share({ + model: new cdb.core.Model(options), + vis: vis, + map: vis.map, + template: template + }); + + widget.createDialog(); + + return widget.render(); + +}); + +// search content +cdb.vis.Overlay.register('search', function(data, vis) { + + var template = cdb.core.Template.compile( + data.template || '\ +
              \ + \ + \ + \ +
              \ + ', + data.templateType || 'mustache' + ); + + var search = new cdb.geo.ui.Search( + _.extend(data, { + template: template, + mapView: vis.mapView, + model: vis.map + }) + ); + + return search.render(); + +}); + +// tooltip +cdb.vis.Overlay.register('tooltip', function(data, vis) { + if (!data.layer && vis.getLayers().length <= 1) { + throw new Error("layer is null"); + } + data.layer = data.layer || vis.getLayers()[1]; + data.layer.setInteraction(true); + data.mapView = vis.mapView; + return new cdb.geo.ui.Tooltip(data); +}); + +cdb.vis.Overlay.register('infobox', function(data, vis) { + var layer; + var layers = vis.getLayers(); + if (!data.layer) { + if(layers.length > 1) { + layer = layers[1]; + } + data.layer = layer; + } + if(!data.layer) { + throw new Error("layer is null"); + } + data.layer.setInteraction(true); + var infobox = new cdb.geo.ui.InfoBox(data); + return infobox; + +}); + +})(); + + +(function() { + +var Layers = cdb.vis.Layers; + +/* + * if we are using http and the tiles of base map need to be fetched from + * https try to fix it + */ + +var HTTPS_TO_HTTP = { + 'https://dnv9my2eseobd.cloudfront.net/': 'http://a.tiles.mapbox.com/', + 'https://maps.nlp.nokia.com/': 'http://maps.nlp.nokia.com/', + 'https://tile.stamen.com/': 'http://tile.stamen.com/', + "https://{s}.maps.nlp.nokia.com/": "http://{s}.maps.nlp.nokia.com/", + "https://cartocdn_{s}.global.ssl.fastly.net/": "http://{s}.api.cartocdn.com/", + "https://cartodb-basemaps-{s}.global.ssl.fastly.net/": "http://{s}.basemaps.cartocdn.com/" +}; + +function transformToHTTP(tilesTemplate) { + for(var url in HTTPS_TO_HTTP) { + if(tilesTemplate.indexOf(url) !== -1) { + return tilesTemplate.replace(url, HTTPS_TO_HTTP[url]) + } + } + return tilesTemplate; +} + +function transformToHTTPS(tilesTemplate) { + for(var url in HTTPS_TO_HTTP) { + var httpsUrl = HTTPS_TO_HTTP[url]; + if(tilesTemplate.indexOf(httpsUrl) !== -1) { + return tilesTemplate.replace(httpsUrl, url); + } + } + return tilesTemplate; +} + +Layers.register('tilejson', function(vis, data) { + var url = data.tiles[0]; + if(vis.https === true) { + url = transformToHTTPS(url); + } + else if(vis.https === false) { // Checking for an explicit false value. If it's undefined the url is left as is. + url = transformToHTTP(url); + } + return new cdb.geo.TileLayer({ + urlTemplate: url + }); +}); + +Layers.register('tiled', function(vis, data) { + var url = data.urlTemplate; + if(vis.https === true) { + url = transformToHTTPS(url); + } + else if(vis.https === false) { // Checking for an explicit false value. If it's undefined the url is left as is. + url = transformToHTTP(url); + } + + data.urlTemplate = url; + return new cdb.geo.TileLayer(data); +}); + +Layers.register('wms', function(vis, data) { + return new cdb.geo.WMSLayer(data); +}); + +Layers.register('gmapsbase', function(vis, data) { + return new cdb.geo.GMapsBaseLayer(data); +}); + +Layers.register('plain', function(vis, data) { + return new cdb.geo.PlainLayer(data); +}); + +Layers.register('background', function(vis, data) { + return new cdb.geo.PlainLayer(data); +}); + + +function normalizeOptions(vis, data) { + if(data.infowindow && data.infowindow.fields) { + if(data.interactivity) { + if(data.interactivity.indexOf('cartodb_id') === -1) { + data.interactivity = data.interactivity + ",cartodb_id"; + } + } else { + data.interactivity = 'cartodb_id'; + } + } + // if https is forced + if(vis.https) { + data.tiler_protocol = 'https'; + data.tiler_port = 443; + data.sql_api_protocol = 'https'; + data.sql_api_port = 443; + } + data.cartodb_logo = vis.cartodb_logo == undefined ? data.cartodb_logo : vis.cartodb_logo; +} + +var cartoLayer = function(vis, data) { + normalizeOptions(vis, data); + // if sublayers are included that means a layergroup should + // be created + if(data.sublayers) { + data.type = 'layergroup'; + return new cdb.geo.CartoDBGroupLayer(data); + } + return new cdb.geo.CartoDBLayer(data); +}; + +Layers.register('cartodb', cartoLayer); +Layers.register('carto', cartoLayer); + +// TODO: This can be removed +Layers.register('layergroup', function(vis, data) { + normalizeOptions(vis, data); + return new cdb.geo.CartoDBGroupLayer(data); +}); + +// TODO: This can be removed +Layers.register('namedmap', function(vis, data) { + normalizeOptions(vis, data); + return new cdb.geo.CartoDBNamedMapLayer(data); +}); + +Layers.register('torque', function(vis, data) { + normalizeOptions(vis, data); + // default is https + if(vis.https) { + if(data.sql_api_domain && data.sql_api_domain.indexOf('cartodb.com') !== -1) { + data.sql_api_protocol = 'https'; + data.sql_api_port = 443; + data.tiler_protocol = 'https'; + data.tiler_port = 443; + } + } + data.cartodb_logo = vis.cartodb_logo == undefined ? data.cartodb_logo : vis.cartodb_logo; + return new cdb.geo.TorqueLayer(data); +}); + +})(); + +/** + * public api for cartodb + */ +(function() { + function _Promise() { + + } + _.extend(_Promise.prototype, Backbone.Events, { + done: function(fn) { + return this.bind('done', fn); + }, + error: function(fn) { + return this.bind('error', fn); + } + }); + + cdb._Promise = _Promise; + + var _requestCache = {}; + + /** + * compose cartodb url + */ + function cartodbUrl(opts) { + var host = opts.host || 'cartodb.com'; + var protocol = opts.protocol || 'https'; + return protocol + '://' + opts.user + '.' + host + '/api/v1/viz/' + opts.table + '/viz.json'; + } + + /** + * given layer params fetchs the layer json + */ + function _getLayerJson(layer, callback) { + var url = null; + if(layer.layers !== undefined || ((layer.kind || layer.type) !== undefined)) { + // layer object contains the layer data + _.defer(function() { callback(layer); }); + return; + } else if(layer.table !== undefined && layer.user !== undefined) { + // layer object points to cartodbjson + url = cartodbUrl(layer); + } else if(layer.indexOf) { + // fetch from url + url = layer; + } + if(url) { + cdb.core.Loader.get(url, callback); + } else { + _.defer(function() { callback(null); }); + } + } + + /** + * create a layer for the specified map + * + * @param map should be a L.Map object, or equivalent depending on what provider you have. + * @param layer should be an url or a javascript object with the data to create the layer + * @param options layer options + * + */ + cartodb.createLayer = function(map, layer, options, callback) { + if(map === undefined) { + throw new TypeError("map should be provided"); + } + if(layer === undefined) { + throw new TypeError("layer should be provided"); + } + + var layerView, MapType; + var options = options || {}; + var args = arguments; + var fn = args[args.length -1]; + if(_.isFunction(fn)) { + callback = fn; + } + var promise = new _Promise(); + + promise.addTo = function(map, position) { + promise.on('done', function() { + MapType.addLayerToMap(layerView, map, position); + }); + return promise; + }; + + _getLayerJson(layer, function(visData) { + + var layerData; + + if(!visData) { + promise.trigger('error'); + return; + } + + // extract layer data from visualization data + if(visData.layers) { + if(visData.layers.length < 2) { + promise.trigger('error', "visualization file does not contain layer info"); + } + var index = options.layerIndex; + if (index !== undefined) { + if(visData.layers.length <= index) { + promise.trigger('error', 'layerIndex out of bounds'); + return; + } + layerData = visData.layers[index]; + } else { + var DATA_LAYER_TYPES = ['namedmap', 'layergroup', 'torque']; + + // Select the first data layer (namedmap or layergroup) + layerData = _.find(visData.layers, function(layer){ + return DATA_LAYER_TYPES.indexOf(layer.type) !== -1; + }); + } + } else { + layerData = visData; + } + + if(!layerData) { + promise.trigger('error'); + return; + } + + // update options + if(options && !_.isFunction(options)) { + layerData.options = layerData.options || {}; + _.extend(layerData.options, options); + } + + options = _.defaults(options, { + infowindow: true, + https: false, + legends: true, + time_slider: true, + tooltip: true + }); + + // check map type + // TODO: improve checking + if(typeof(map.overlayMapTypes) !== "undefined") { + MapType = cdb.geo.GoogleMapsMapView; + // check if leaflet is loaded globally + } else if(map instanceof L.Map || (window.L && map instanceof window.L.Map)) { + MapType = cdb.geo.LeafletMapView; + } else { + promise.trigger('error', "cartodb.js can't guess the map type"); + return promise; + } + + // create a dummy viz + var viz = map.viz; + if(!viz) { + var mapView = new MapType({ + map_object: map, + map: new cdb.geo.Map() + }); + + map.viz = viz = new cdb.vis.Vis({ + mapView: mapView + }); + + viz.updated_at = visData.updated_at; + viz.https = options.https; + } + + function createLayer() { + layerView = viz.createLayer(layerData, { no_base_layer: true }); + + var torqueLayer; + var mobileEnabled = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + var addMobileLayout = (options.mobile_layout && mobileEnabled) || options.force_mobile; + + if(!layerView) { + promise.trigger('error', "layer not supported"); + return promise; + } + + if(options.infowindow) { + viz.addInfowindow(layerView); + } + + if(options.tooltip) { + viz.addTooltip(layerView); + } + + if(options.legends) { + var layerModel = cdb.vis.Layers.create(layerData.type || layerData.kind, viz, layerData); + + viz._addLegends(viz._createLayerLegendView(layerModel.attributes, layerView)) + } + + if(options.time_slider && layerView.model.get('type') === 'torque') { + + if (!addMobileLayout) { // don't add the overlay if we are in mobile + viz.addTimeSlider(layerView); + } + + torqueLayer = layerView; + } + + if (addMobileLayout) { + + options.mapView = map.viz.mapView; + + viz.addOverlay({ + type: 'mobile', + layerView: layerView, + overlays: [], + torqueLayer: torqueLayer, + options: options + }); + } + + callback && callback(layerView); + promise.trigger('done', layerView); + } + + // load needed modules + if(!viz.checkModules([layerData])) { + viz.loadModules([layerData], function() { + createLayer(); + }); + } else { + createLayer(); + } + }); + + return promise; + }; +})(); + + +;(function() { + + var root = this; + + root.cartodb = root.cartodb || {}; + + function SQL(options) { + if(cartodb === this || window === this) { + return new SQL(options); + } + if(!options.user) { + throw new Error("user should be provided"); + } + var loc = new String(window.location.protocol); + loc = loc.slice(0, loc.length - 1); + if(loc == 'file') { + loc = 'https'; + } + + this.ajax = options.ajax || (typeof(jQuery) !== 'undefined' ? jQuery.ajax: reqwest); + if(!this.ajax) { + throw new Error("jQuery or reqwest should be loaded"); + } + + this.options = _.defaults(options, { + version: 'v2', + protocol: loc, + jsonp: typeof(jQuery) !== 'undefined' ? !jQuery.support.cors: false + }) + + if (!this.options.sql_api_template) { + var opts = this.options; + var template = null; + if(opts && opts.completeDomain) { + template = opts.completeDomain; + } else { + var host = opts.host || 'cartodb.com'; + var protocol = opts.protocol || 'https'; + template = protocol + '://{user}.' + host; + } + this.options.sql_api_template = template; + } + } + + SQL.prototype._host = function() { + var opts = this.options; + return opts.sql_api_template.replace('{user}', opts.user) + '/api/' + opts.version + '/sql'; + }, + + /** + * var sql = new SQL('cartodb_username'); + * sql.execute("select * from {{ table }} where id = {{ id }}", { + * table: 'test', + * id: '1' + * }) + */ + SQL.prototype.execute = function(sql, vars, options, callback) { + + //Variable that defines if a query should be using get method or post method + var MAX_LENGTH_GET_QUERY = 1024; + + var promise = new cartodb._Promise(); + if(!sql) { + throw new TypeError("sql should not be null"); + } + // setup arguments + var args = arguments, + fn = args[args.length -1]; + if(_.isFunction(fn)) { + callback = fn; + } + options = _.defaults(options || {}, this.options); + var params = { + type: 'get', + dataType: 'json', + crossDomain: true + }; + + if(options.cache !== undefined) { + params.cache = options.cache; + } + + if(options.jsonp) { + delete params.crossDomain; + if (options.jsonpCallback) { + params.jsonpCallback = options.jsonpCallback; + } + params.dataType = 'jsonp'; + } + + // Substitute mapnik tokens + // resolution at zoom level 0 + var res = '156543.03515625'; + // full webmercator extent + var ext = 'ST_MakeEnvelope(-20037508.5,-20037508.5,20037508.5,20037508.5,3857)'; + sql = sql.replace('!bbox!', ext) + .replace('!pixel_width!', res) + .replace('!pixel_height!', res); + + // create query + var query = Mustache.render(sql, vars); + + // check method: if we are going to send by get or by post + var isGetRequest = query.length < MAX_LENGTH_GET_QUERY; + + // generate url depending on the http method + var reqParams = ['format', 'dp', 'api_key']; + // request params + if (options.extra_params) { + reqParams = reqParams.concat(options.extra_params); + } + + params.url = this._host() ; + if (isGetRequest) { + var q = 'q=' + encodeURIComponent(query); + for(var i in reqParams) { + var r = reqParams[i]; + var v = options[r]; + if(v) { + q += '&' + r + "=" + v; + } + } + + params.url += '?' + q; + } else { + var objPost = {'q': query}; + for(var i in reqParams) { + var r = reqParams[i]; + var v = options[r]; + if (v) { + objPost[r] = v; + } + } + + params.data = objPost; + //Check if we are using jQuery(uncompressed) or reqwest (core) + if ((typeof(jQuery) !== 'undefined')) { + params.type = 'post'; + } else { + params.method = 'post'; + } + } + + // wrap success and error functions + var success = options.success; + var error = options.error; + if(success) delete options.success; + if(error) delete error.success; + + params.error = function(resp) { + var res = resp.responseText || resp.response; + var errors = res && JSON.parse(res); + promise.trigger('error', errors && errors.error, resp) + if(error) error(resp); + } + params.success = function(resp, status, xhr) { + // manage rewest + if(status == undefined) { + status = resp.status; + xhr = resp; + resp = JSON.parse(resp.response); + } + //Timeout explanation. CartoDB.js ticket #336 + //From St.Ov.: "what setTimeout does is add a new event to the browser event queue + //and the rendering engine is already in that queue (not entirely true, but close enough) + //so it gets executed before the setTimeout event." + setTimeout(function() { + promise.trigger('done', resp, status, xhr); + if(success) success(resp, status, xhr); + if(callback) callback(resp); + }, 0); + } + + // call ajax + delete options.jsonp; + this.ajax(_.extend(params, options)); + return promise; + } + + SQL.prototype.getBounds = function(sql, vars, options, callback) { + var promise = new cartodb._Promise(); + var args = arguments, + fn = args[args.length -1]; + if(_.isFunction(fn)) { + callback = fn; + } + var s = 'SELECT ST_XMin(ST_Extent(the_geom)) as minx,' + + ' ST_YMin(ST_Extent(the_geom)) as miny,'+ + ' ST_XMax(ST_Extent(the_geom)) as maxx,' + + ' ST_YMax(ST_Extent(the_geom)) as maxy' + + ' from ({{{ sql }}}) as subq'; + sql = Mustache.render(sql, vars); + this.execute(s, { sql: sql }, options) + .done(function(result) { + if (result.rows && result.rows.length > 0 && result.rows[0].maxx != null) { + var c = result.rows[0]; + var minlat = -85.0511; + var maxlat = 85.0511; + var minlon = -179; + var maxlon = 179; + + var clamp = function(x, min, max) { + return x < min ? min : x > max ? max : x; + } + + var lon0 = clamp(c.maxx, minlon, maxlon); + var lon1 = clamp(c.minx, minlon, maxlon); + var lat0 = clamp(c.maxy, minlat, maxlat); + var lat1 = clamp(c.miny, minlat, maxlat); + + var bounds = [[lat0, lon0], [lat1, lon1]]; + promise.trigger('done', bounds); + callback && callback(bounds); + } + }) + .error(function(err) { + promise.trigger('error', err); + }) + + return promise; + + } + + /** + * var people_under_10 = sql + * .table('test') + * .columns(['age', 'column2']) + * .filter('age < 10') + * .limit(15) + * .order_by('age') + * + * people_under_10(function(results) { + * }) + */ + + SQL.prototype.table = function(name) { + + var _name = name; + var _filters; + var _columns = []; + var _limit; + var _order; + var _orderDir; + var _sql = this; + + function _table() { + _table.fetch.apply(_table, arguments); + } + + _table.fetch = function(vars) { + vars = vars || {} + var args = arguments, + fn = args[args.length -1]; + if(_.isFunction(fn)) { + callback = fn; + if(args.length === 1) vars = {}; + } + _sql.execute(_table.sql(), vars, callback); + } + + _table.sql = function() { + var s = "select" + if(_columns.length) { + s += ' ' + _columns.join(',') + ' ' + } else { + s += ' * ' + } + + s += "from " + _name; + + if(_filters) { + s += " where " + _filters; + } + if(_limit) { + s += " limit " + _limit; + } + if(_order) { + s += " order by " + _order; + } + if(_orderDir) { + s += ' ' + _orderDir; + } + + return s; + } + + _table.filter = function(f) { + _filters = f; + return _table; + } + + _table.order_by= function(o) { + _order = o; + return _table; + } + _table.asc = function() { + _orderDir = 'asc' + return _table; + } + + _table.desc = function() { + _orderDir = 'desc' + return _table; + } + + _table.columns = function(c) { + _columns = c; + return _table; + } + + _table.limit = function(l) { + _limit = l; + return _table; + } + + return _table; + + } + + + /* + * sql.filter(sql.f().distance('< 10km') + */ + /*cartodb.SQL.geoFilter = function() { + var _sql; + function f() {} + + f.distance = function(qty) { + qty.replace('km', '*1000') + _sql += 'st_distance(the_geom) ' + qty + } + f.or = function() { + } + + f.and = function() { + } + return f; + } + */ + function array_agg(s) { + return JSON.parse(s.replace(/^{/, '[').replace(/}$/,']')); + } + + + SQL.prototype.describeString = function(sql, column, callback) { + + var s = [ + 'WITH t as (', + ' SELECT count(*) as total,', + ' count(DISTINCT {{column}}) as ndist', + ' FROM ({{sql}}) _wrap', + ' ), a as (', + ' SELECT ', + ' count(*) cnt, ', + ' {{column}}', + ' FROM ', + ' ({{sql}}) _wrap ', + ' GROUP BY ', + ' {{column}} ', + ' ORDER BY ', + ' cnt DESC', + ' ), b As (', + ' SELECT', + ' row_number() OVER (ORDER BY cnt DESC) rn,', + ' cnt', + ' FROM a', + ' ), c As (', + ' SELECT ', + ' sum(cnt) OVER (ORDER BY rn ASC) / t.total cumperc,', + ' rn,', + ' cnt ', + ' FROM b, t', + ' LIMIT 10', + ' ),', + 'stats as (', + 'select count(distinct({{column}})) as uniq, ', + ' count(*) as cnt, ', + ' sum(case when COALESCE(NULLIF({{column}},\'\')) is null then 1 else 0 end)::numeric as null_count, ', + ' sum(case when COALESCE(NULLIF({{column}},\'\')) is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio, ', + // ' CDB_DistinctMeasure(array_agg({{column}}::text)) as cat_weight ', + ' (SELECT max(cumperc) weight FROM c) As skew ', + 'from ({{sql}}) __wrap', + '),', + 'hist as (', + 'select array_agg(row(d, c)) array_agg from (select distinct({{column}}) d, count(*) as c from ({{sql}}) __wrap, stats group by 1 limit 100) _a', + ')', + 'select * from stats, hist' + ]; + + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + var normalizeName = function(str) { + var normalizedStr = str.replace(/^"(.+(?="$))?"$/, '$1'); // removes surrounding quotes + return normalizedStr.replace(/""/g, '"'); // removes duplicated quotes + } + + this.execute(query, function(data) { + var row = data.rows[0]; + var weight = 0; + var histogram = []; + + try { + var s = array_agg(row.array_agg); + + var histogram = _(s).map(function(row) { + var r = row.match(/\((.*),(\d+)/); + var name = normalizeName(r[1]); + return [name, +r[2]]; + }); + + weight = row.skew * (1 - row.null_ratio) * (1 - row.uniq / row.cnt) * ( row.uniq > 1 ? 1 : 0); + } catch(e) { + + } + + callback({ + type: 'string', + hist: histogram, + distinct: row.uniq, + count: row.cnt, + null_count: row.null_count, + null_ratio: row.null_ratio, + skew: row.skew, + weight: weight + }); + }); + } + + SQL.prototype.describeDate = function(sql, column, callback) { + var s = [ + 'with minimum as (', + 'SELECT min({{column}}) as start_time FROM ({{sql}}) _wrap), ', + 'maximum as (SELECT max({{column}}) as end_time FROM ({{sql}}) _wrap), ', + 'null_ratio as (SELECT sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio FROM ({{sql}}) _wrap), ', + 'moments as (SELECT count(DISTINCT {{column}}) as moments FROM ({{sql}}) _wrap)', + 'SELECT * FROM minimum, maximum, moments, null_ratio' + ]; + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + this.execute(query, function(data) { + var row = data.rows[0]; + var e = new Date(row.end_time); + var s = new Date(row.start_time); + + var moments = row.moments; + + var steps = Math.min(row.moments, 1024); + + callback({ + type: 'date', + start_time: s, + end_time: e, + range: e - s, + steps: steps, + null_ratio: row.null_ratio + }); + }); + } + + SQL.prototype.describeBoolean = function(sql, column, callback){ + var s = [ + 'with stats as (', + 'select count(distinct({{column}})) as uniq,', + 'count(*) as cnt', + 'from ({{sql}}) _wrap ', + '),', + 'null_ratio as (', + 'SELECT sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio FROM ({{sql}}) _wrap), ', + 'true_ratio as (', + 'SELECT sum(case when {{column}} is true then 1 else 0 end)::numeric / count(*)::numeric as true_ratio FROM ({{sql}}) _wrap) ', + 'SELECT * FROM true_ratio, null_ratio, stats' + ]; + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + this.execute(query, function(data) { + var row = data.rows[0]; + + callback({ + type: 'boolean', + null_ratio: row.null_ratio, + true_ratio: row.true_ratio, + distinct: row.uniq, + count: row.cnt + }); + }); + } + + SQL.prototype.describeGeom = function(sql, column, callback) { + var s = [ + 'with stats as (', + 'select st_asgeojson(st_extent({{column}})) as bbox', + 'from ({{sql}}) _wrap', + '),', + 'geotype as (', + 'select st_geometrytype({{column}}) as geometry_type from ({{sql}}) _w where {{column}} is not null limit 1', + '),', + 'clusters as (', + 'with clus as (', + 'SELECT distinct(ST_snaptogrid(the_geom, 10)) as cluster, count(*) as clustercount FROM ({{sql}}) _wrap group by 1 order by 2 desc limit 3),', + 'total as (', + 'SELECT count(*) FROM ({{sql}}) _wrap)', + 'SELECT sum(clus.clustercount)/sum(total.count) AS clusterrate FROM clus, total', + '),', + 'density as (', + 'SELECT count(*) / st_area(st_extent(the_geom)) as density FROM ({{sql}}) _wrap', + ')', + 'select * from stats, geotype, clusters, density' + ]; + + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + function simplifyType(g) { + return { + 'st_multipolygon': 'polygon', + 'st_polygon': 'polygon', + 'st_multilinestring': 'line', + 'st_linestring': 'line', + 'st_multipoint': 'point', + 'st_point': 'point' + }[g.toLowerCase()] + }; + + this.execute(query, function(data) { + var row = data.rows[0]; + var bbox = JSON.parse(row.bbox).coordinates[0] + callback({ + type: 'geom', + //lon,lat -> lat, lon + bbox: [[bbox[0][0],bbox[0][1]], [bbox[2][0], bbox[2][1]]], + geometry_type: row.geometry_type, + simplified_geometry_type: simplifyType(row.geometry_type), + cluster_rate: row.clusterrate, + density: row.density + }); + }); + } + + SQL.prototype.columns = function(sql, options, callback) { + var args = arguments, + fn = args[args.length -1]; + if(_.isFunction(fn)) { + callback = fn; + } + var s = "select * from (" + sql + ") __wrap limit 0"; + var exclude = ['cartodb_id','latitude','longitude','created_at','updated_at','lat','lon','the_geom_webmercator']; + this.execute(s, function(data) { + var t = {} + for (var i in data.fields) { + if (exclude.indexOf(i) === -1) { + t[i] = data.fields[i].type; + } + } + callback(t); + }); + }; + + SQL.prototype.describeFloat = function(sql, column, callback) { + var s = [ + 'with stats as (', + 'select min({{column}}) as min,', + 'max({{column}}) as max,', + 'avg({{column}}) as avg,', + 'count(DISTINCT {{column}}) as cnt,', + 'count(distinct({{column}})) as uniq,', + 'count(*) as cnt,', + 'sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio,', + 'stddev_pop({{column}}) / count({{column}}) as stddev,', + 'CASE WHEN abs(avg({{column}})) > 1e-7 THEN stddev({{column}}) / abs(avg({{column}})) ELSE 1e12 END as stddevmean,', + 'CDB_DistType(array_agg("{{column}}"::numeric)) as dist_type ', + 'from ({{sql}}) _wrap ', + '),', + 'params as (select min(a) as min, (max(a) - min(a)) / 7 as diff from ( select {{column}} as a from ({{sql}}) _table_sql where {{column}} is not null ) as foo ),', + 'histogram as (', + 'select array_agg(row(bucket, range, freq)) as hist from (', + 'select CASE WHEN uniq > 1 then width_bucket({{column}}, min-0.01*abs(min), max+0.01*abs(max), 100) ELSE 1 END as bucket,', + 'numrange(min({{column}})::numeric, max({{column}})::numeric) as range,', + 'count(*) as freq', + 'from ({{sql}}) _w, stats', + 'group by 1', + 'order by 1', + ') __wrap', + '),', + 'hist as (', + 'select array_agg(row(d, c)) cat_hist from (select distinct({{column}}) d, count(*) as c from ({{sql}}) __wrap, stats group by 1 limit 100) _a', + '),', + 'buckets as (', + 'select CDB_QuantileBins(array_agg(distinct({{column}}::numeric)), 7) as quantiles, ', + ' (select array_agg(x::numeric) FROM (SELECT (min + n * diff)::numeric as x FROM generate_series(1,7) n, params) p) as equalint,', + // ' CDB_EqualIntervalBins(array_agg({{column}}::numeric), 7) as equalint, ', + ' CDB_JenksBins(array_agg(distinct({{column}}::numeric)), 7) as jenks, ', + ' CDB_HeadsTailsBins(array_agg(distinct({{column}}::numeric)), 7) as headtails ', + 'from ({{sql}}) _table_sql where {{column}} is not null', + ')', + 'select * from histogram, stats, buckets, hist' + ]; + + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + this.execute(query, function(data) { + var row = data.rows[0]; + var s = array_agg(row.hist); + var h = array_agg(row.cat_hist); + callback({ + type: 'number', + cat_hist: + _(h).map(function(row) { + var r = row.match(/\((.*),(\d+)/); + return [+r[1], +r[2]]; + }), + hist: _(s).map(function(row) { + if(row.indexOf("empty") > -1) return; + var els = row.split('"'); + return { index: els[0].replace(/\D/g,''), + range: els[1].split(",").map(function(d){return d.replace(/\D/g,'')}), + freq: els[2].replace(/\D/g,'') }; + }), + stddev: row.stddev, + null_ratio: row.null_ratio, + count: row.cnt, + distinct: row.uniq, + //lstddev: row.lstddev, + avg: row.avg, + max: row.max, + min: row.min, + stddevmean: row.stddevmean, + weight: (row.uniq > 1 ? 1 : 0) * (1 - row.null_ratio) * (row.stddev < -1 ? 1 : (row.stddev < 1 ? 0.5 : (row.stddev < 3 ? 0.25 : 0.1))), + quantiles: row.quantiles, + equalint: row.equalint, + jenks: row.jenks, + headtails: row.headtails, + dist_type: row.dist_type + }); + }); + } + + // describe a column + SQL.prototype.describe = function(sql, column, options) { + var self = this; + var args = arguments, + fn = args[args.length -1]; + if(_.isFunction(fn)) { + var _callback = fn; + } + var callback = function(data) { + data.column = column; + _callback(data); + } + var s = "select * from (" + sql + ") __wrap limit 0"; + this.execute(s, function(data) { + + var type = (options && options.type) ? options.type : data.fields[column].type; + + if (!type) { + callback(new Error("column does not exist")); + return; + } + + else if (type === 'string') { + self.describeString(sql, column, callback); + } else if (type === 'number') { + self.describeFloat(sql, column, callback); + } else if (type === 'geometry') { + self.describeGeom(sql, column, callback); + } else if (type === 'date') { + self.describeDate(sql, column, callback); + } else if (type === 'boolean') { + self.describeBoolean(sql, column, callback); + } else { + callback(new Error("column type is not supported")); + } + }); + } + + root.cartodb.SQL = SQL; + +})(); + +(function() { + + cartodb.createVis = function(el, vizjson, options, callback) { + + if (!el) { + throw new TypeError("a DOM element should be provided"); + } + + var + args = arguments, + fn = args[args.length -1]; + + if (_.isFunction(fn)) { + callback = fn; + } + + el = (typeof el === 'string' ? document.getElementById(el) : el); + + var vis = new cartodb.vis.Vis({ el: el }); + + if (vizjson) { + + vis.load(vizjson, options); + + if (callback) { + vis.done(callback); + } + + } + + return vis; + + }; + +})(); + +cdb.windshaft.config = {}; +cdb.windshaft.config.MAPS_API_BASE_URL = 'api/v1/map' +/** + * Windshaft client. It provides a method to create instances of dashboards. + * @param {object} options Options to set up the client + */ +cdb.windshaft.Client = function(options) { + this.ajax = window.$ ? window.$.ajax : reqwest.compat; + this.windshaftURLTemplate = options.windshaftURLTemplate; + this.userName = options.userName; + this.url = this.windshaftURLTemplate.replace('{user}', this.userName); + this.statTag = options.statTag; + this.isCorsSupported = cdb.core.util.isCORSSupported(); + this.forceCors = options.forceCors; + this.endpoint = options.endpoint; +} + +cdb.windshaft.Client.DEFAULT_COMPRESSION_LEVEL = 3; +cdb.windshaft.Client.MAX_GET_SIZE = 2033; + +/** + * Creates an instance of a map in Windshaft + * @param {object} mapDefinition An object that responds to .toJSON with the definition of the map + * @param {function} callback A callback that will get the public or private map + * @return {cdb.windshaft.DashboardInstance} The instance of the dashboard + */ +cdb.windshaft.Client.prototype.instantiateMap = function(mapDefinition) { + var payload = JSON.stringify(mapDefinition); + + var dashboardInstance = new cdb.windshaft.DashboardInstance(); + + var options = { + success: function(data) { + if (data.errors) { + // TODO: Error handling + throw data.errors; + } else { + data.windshaftURLTemplate = this.windshaftURLTemplate; + data.userName = this.userName; + dashboardInstance.set(data); + } + }.bind(this), + error: function(xhr) { + var err = { errors: ['Unknown error'] }; + try { + err = JSON.parse(xhr.responseText); + } catch(e) {} + throw err.errors; + } + } + + if (this._usePOST(payload)) { + this._post(payload, options); + } else { + this._get(payload, options); + } + + return dashboardInstance; +} + +cdb.windshaft.Client.prototype._usePOST = function(payload) { + if (this.isCorsSupported && this.forceCors) { + return true; + } + return payload.length >= this.constructor.MAX_GET_SIZE; +} + +cdb.windshaft.Client.prototype._post = function(payload, options) { + this.ajax({ + crossOrigin: true, + type: 'POST', + method: 'POST', + dataType: 'json', + contentType: 'application/json', + url: this._getURL(), + data: payload, + success: options.success, + error: options.error + }); +} + +cdb.windshaft.Client.prototype._get = function(payload, options) { + var compressFunction = this._getCompressor(payload); + compressFunction(payload, this.constructor.DEFAULT_COMPRESSION_LEVEL, function(dataParameter) { + this.ajax({ + url: this._getURL(dataParameter), + dataType: 'jsonp', + jsonpCallback: this._jsonpCallbackName(payload), + cache: true, + success: options.success, + error: options.error + }); + }.bind(this)); +} + +cdb.windshaft.Client.prototype._getCompressor = function(payload) { + if (payload.length < this.constructor.MAX_GET_SIZE) { + return function(data, level, callback) { + callback("config=" + encodeURIComponent(data)); + }; + } + + return function(data, level, callback) { + data = JSON.stringify({ config: data }); + LZMA.compress(data, level, function(encoded) { + callback("lzma=" + encodeURIComponent(cdb.core.util.array2hex(encoded))); + }); + }; +} + + +cdb.windshaft.Client.prototype._getURL = function(dataParameter) { + var params = []; + params.push(["stat_tag", this.statTag].join("=")); + if (dataParameter) { + params.push(dataParameter); + } + return [this.url, this.endpoint].join('/') + '?' + params.join('&'); +} + +cdb.windshaft.Client.prototype._jsonpCallbackName = function(payload) { + return '_cdbc_' + cdb.core.util.uniqueCallbackName(payload); +} +cdb.windshaft.Dashboard = function(options) { + this.cartoDBLayerGroup = options.cartoDBLayerGroup; + this.widgets = options.widgets; + this.client = options.client; + this.statTag = options.statTag; + this.configGenerator = options.configGenerator; + + this.instance = new cdb.windshaft.DashboardInstance(); + this.cartoDBLayerGroup.bindDashboardInstance(this.instance); + + // Bindings + this.cartoDBLayerGroup.layers.bind('change', this.createInstance, this); + this.instance.bind('change:layergroupid', this._updateWidgets, this); +}; + +cdb.windshaft.Dashboard.prototype._updateWidgets = function(dashboardInstance) { + _.each(this.widgets, function(widget) { + widget.set({ + dashboardBaseURL: dashboardInstance.getBaseURL(), + }); + }.bind(this)); +}; + +cdb.windshaft.Dashboard.prototype.createInstance = function() { + var dashboardConfig = this.configGenerator.generate(this); + var instance = this.client.instantiateMap(dashboardConfig); + instance.bind('change:layergroupid', function() { + this.instance.set(instance.toJSON()); + }.bind(this)); + + return this.instance; +}; + +cdb.windshaft.Dashboard.prototype.getLayers = function() { + return this.cartoDBLayerGroup.layers; +}; + +cdb.windshaft.Dashboard.prototype.getVisibleLayers = function() { + return this.cartoDBLayerGroup.getVisibleLayers(); +}; + +cdb.windshaft.Dashboard.prototype.getWidgets = function() { + return this.widgets; +}; + +cdb.windshaft.DashboardInstance = cdb.core.Model.extend({ + + initialize: function() { + this.ajax = $.ajax; + + // TODO: What params are really used? + this.pngParams = ['map_key', 'api_key', 'cache_policy', 'updated_at']; + this.gridParams = ['map_key', 'api_key', 'cache_policy', 'updated_at']; + }, + + getMapId: function() { + return this.get('layergroupid'); + }, + + getBaseURL: function() { + return [ + this.getHost(), + cdb.windshaft.config.MAPS_API_BASE_URL, + this.getMapId(), + ].join('/'); + }, + + getHost: function(subhost) { + var userName = this.get('userName'); + var protocol = this._useHTTPS() ? 'https' : 'http'; + var subhost = subhost || ''; + var host = this.get('windshaftURLTemplate').replace('{user}', userName); + var cdnHost = this.get('cdn_url') && this.get('cdn_url')[protocol]; + if (cdnHost) { + host = [protocol, '://', subhost, cdnHost, '/', userName].join(''); + } + + return host; + }, + + _useHTTPS: function() { + return this.get('windshaftURLTemplate').indexOf('https') === 0; + }, + + /** + * Returns the index of a layer of a given type, as the tiler kwows it. + * + * @param {integer} index - number of layer of the specified type + * @param {string} layerType - type of the layers + */ + getLayerIndexByType: function(index, layerType) { + var layers = this.get('metadata') && this.get('metadata').layers; + + if (!layers) { + return index; + } + + var tilerLayerIndex = {}; + var j = 0; + for (var i = 0; i < layers.length; i++) { + if (layers[i].type == layerType) { + tilerLayerIndex[j] = i; + j++; + } + } + if (tilerLayerIndex[index] === undefined) { + return -1; + } + return tilerLayerIndex[index]; + }, + + /** + * Returns the index of a layer of a given type, as the tiler kwows it. + * + * @param {string|array} types - Type or types of layers + */ + getLayerIndexesByType: function(types) { + var layers = this.get('metadata') && this.get('metadata').layers; + + if (!layers) { + return; + } + var layerIndexes = []; + for (var i = 0; i < layers.length; i++) { + var layer = layers[i]; + var isValidType = layer.type !== 'torque'; + if (types && types.length > 0) { + isValidType = isValidType && types.indexOf(layer.type) != -1; + } + if (isValidType) { + layerIndexes.push(i); + } + } + return layerIndexes; + }, + + getTiles: function() { + var grids = []; + var tiles = []; + var params = []; + var pngParams = this._encodeParams(params, this.pngParams); + var gridParams = this._encodeParams(params, this.gridParams); + var subdomains = ['0', '1', '2', '3']; + + if(this._useHTTPS()) { + subdomains = ['']; + } + + var layerIndexes = this.getLayerIndexesByType("mapnik"); + if (layerIndexes.length) { + var tileTemplate = '/' + layerIndexes.join(',') +'/{z}/{x}/{y}'; + var gridTemplate = '/{z}/{x}/{y}'; + + for(var i = 0; i < subdomains.length; ++i) { + var s = subdomains[i]; + var cartodb_url = this.getHost(s) + MapBase.BASE_URL + '/' + this.getMapId(); + tiles.push(cartodb_url + tileTemplate + ".png" + (pngParams ? "?" + pngParams: '') ); + + for(var layer = 0; layer < this.get('metadata').layers.length; ++layer) { + var index = this.getLayerIndexByType(layer, "mapnik"); + grids[layer] = grids[layer] || []; + grids[layer].push(cartodb_url + "/" + index + gridTemplate + ".grid.json" + (gridParams ? "?" + gridParams: '')); + } + } + } else { + tiles = [MapBase.EMPTY_GIF]; + } + + this.urls = { + tiles: tiles, + grids: grids + }; + return this.urls; + }, + + _encodeParams: function(params, included) { + if(!params) return ''; + var url_params = []; + included = included || _.keys(params); + for(var i in included) { + var k = included[i]; + var p = params[k]; + if(p) { + if (_.isArray(p)) { + for (var j = 0, len = p.length; j < len; j++) { + url_params.push(k + "[]=" + encodeURIComponent(p[j])); + } + } else { + var q = encodeURIComponent(p); + q = q.replace(/%7Bx%7D/g,"{x}").replace(/%7By%7D/g,"{y}").replace(/%7Bz%7D/g,"{z}"); + url_params.push(k + "=" + q); + } + } + } + return url_params.join('&'); + }, + + fetchAttributes: function(layer, featureID, callback) { + var url = [ + this.getBaseURL(), + this.getLayerIndexByType(layer, "mapnik"), + 'attributes', + featureID + ].join('/'); + + $.ajax({ + dataType: 'jsonp', + url: url, + jsonpCallback: '_cdbi_layer_attributes_' + cdb.core.util.uniqueCallbackName(this.toJSON()), + cache: true, + success: function(data) { + // loadingTime.end(); + callback(data); + }, + error: function(data) { + // loadingTime.end(); + // cartodb.core.Profiler.metric('cartodb-js.named_map.attributes.error').inc(); + callback(null); + } + }); + } +}) + +cdb.windshaft.PublicDashboardConfig = {}; + +cdb.windshaft.PublicDashboardConfig.generate = function(dashboard) { + var config = {}; + + // LAYERS + config.layers = _.map(dashboard.getVisibleLayers(), function(layerModel) { + var layerConfig = { + type: 'cartodb', + options: { + sql: layerModel.get('sql'), + cartocss: layerModel.get('cartocss'), + cartocss_version: layerModel.get('cartocss_version'), + interactivity: layerModel.getInteractiveColumnNames() + } + } + if (layerModel.getInfowindowFieldNames().length) { + layerConfig.options.attributes = { + id: "cartodb_id", + columns: layerModel.getInfowindowFieldNames() + } + } + return layerConfig; + }) + + // WIDGETS + var widgets = dashboard.getWidgets(); + if (widgets && widgets.length) { + config.lists = {}; + + // TODO: Add histograms. + // var lists = _.filter(widgets, function(widget){ + // return widget.get('type') === 'list' + // }); + widgets.forEach(function(list) { + config.lists[list.get('id')] = { + "sql": list.get('options').sql, + "columns": list.get('options').columns + } + }) + } + + return config; +}; +cdb.windshaft.PrivateDashboardConfig = {}; + +cdb.windshaft.PrivateDashboardConfig.generate = function(dashboard) { + var config = {}; + + dashboard.getLayers().each(function(layer, index){ + config['layer' + index] = layer.isVisible() ? '1' : 0; + }); + + // TODO: We should add the params + // TODO: We should add the auth_token + return config; +}; + + cdb.$ = $; + cdb.L = L; + cdb.Mustache = Mustache; + cdb.Backbone = Backbone; + cdb._ = _; + + })(); + + + + + ; + for(var i in __prev) { + // keep it at global context if it didn't exist + if(__prev[i]) { + window[i] = __prev[i]; + } + } + + +})(); diff --git a/examples/vendor/histogram-widget.js b/examples/vendor/histogram-widget.js new file mode 100644 index 00000000..a9723063 --- /dev/null +++ b/examples/vendor/histogram-widget.js @@ -0,0 +1,1277 @@ + +/** + * Default widget view: + * + * It contains: + * - view model (viewModel) + * - data model (dataModel) + * + * It will offet to the user: + * - get current data (getData) + * - filter the current datasource (filter), each view will let + * different possibilities. + * - Sync or unsync widget (sync/unsync), making the proper view + * listen or not changes from the current datasource. + * + */ +cdb ={core:{}, geo:{ui: {Widget:{}}}} + + + +cdb._debugCallbacks= function(o) { + var callbacks = o._callbacks; + for(var i in callbacks) { + var node = callbacks[i]; + console.log(" * ", i); + var end = node.tail; + while ((node = node.next) !== end) { + console.log(" - ", node.context, (node.context && node.context.el) || 'none'); + } + } +} + +/** + * Base Model for all CartoDB model. + * DO NOT USE Backbone.Model directly + * @class cdb.core.Model + */ +var Model = cdb.core.Model = Backbone.Model.extend({ + + initialize: function(options) { + _.bindAll(this, 'fetch', 'save', 'retrigger'); + return Backbone.Model.prototype.initialize.call(this, options); + }, + /** + * We are redefining fetch to be able to trigger an event when the ajax call ends, no matter if there's + * a change in the data or not. Why don't backbone does this by default? ahh, my friend, who knows. + * @method fetch + * @param args {Object} + */ + fetch: function(args) { + var self = this; + // var date = new Date(); + this.trigger('loadModelStarted'); + $.when(this.elder('fetch', args)).done(function(ev){ + self.trigger('loadModelCompleted', ev); + // var dateComplete = new Date() + // console.log('completed in '+(dateComplete - date)); + }).fail(function(ev) { + self.trigger('loadModelFailed', ev); + }) + }, + /** + * Changes the attribute used as Id + * @method setIdAttribute + * @param attr {String} + */ + setIdAttribute: function(attr) { + this.idAttribute = attr; + }, + /** + * Listen for an event on another object and triggers on itself, with the same name or a new one + * @method retrigger + * @param ev {String} event who triggers the action + * @param obj {Object} object where the event happens + * @param obj {Object} [optional] name of the retriggered event; + * @todo [xabel]: This method is repeated here and in the base view definition. There's should be a way to make it unique + */ + retrigger: function(ev, obj, retrigEvent) { + if(!retrigEvent) { + retrigEvent = ev; + } + var self = this; + obj.bind && obj.bind(ev, function() { + self.trigger(retrigEvent); + }, self) + }, + + /** + * We need to override backbone save method to be able to introduce new kind of triggers that + * for some reason are not present in the original library. Because you know, it would be nice + * to be able to differenciate "a model has been updated" of "a model is being saved". + * TODO: remove jquery from here + * @param {object} opt1 + * @param {object} opt2 + * @return {$.Deferred} + */ + save: function(opt1, opt2) { + var self = this; + if(!opt2 || !opt2.silent) this.trigger('saving'); + var promise = Backbone.Model.prototype.save.apply(this, arguments); + $.when(promise).done(function() { + if(!opt2 || !opt2.silent) self.trigger('saved'); + }).fail(function() { + if(!opt2 || !opt2.silent) self.trigger('errorSaving') + }) + return promise; + } +}); + + + + var View = cdb.core.View = Backbone.View.extend({ + classLabel: 'cdb.core.View', + constructor: function(options) { + this.options = _.defaults(options, this.options); + this._models = []; + this._subviews = {}; + Backbone.View.call(this, options); + View.viewCount++; + View.views[this.cid] = this; + this._created_at = new Date(); + }, + + add_related_model: function(m) { + if(!m) throw "added non valid model" + this._models.push(m); + }, + + addView: function(v) { + this._subviews[v.cid] = v; + v._parent = this; + }, + + removeView: function(v) { + delete this._subviews[v.cid]; + }, + + clearSubViews: function() { + _(this._subviews).each(function(v) { + v.clean(); + }); + this._subviews = {}; + }, + + /** + * this methid clean removes the view + * and clean and events associated. call it when + * the view is not going to be used anymore + */ + clean: function() { + var self = this; + this.trigger('clean'); + this.clearSubViews(); + // remove from parent + if(this._parent) { + this._parent.removeView(this); + this._parent = null; + } + this.remove(); + this.unbind(); + // remove this model binding + if (this.model && this.model.unbind) this.model.unbind(null, null, this); + // remove model binding + _(this._models).each(function(m) { + m.unbind(null, null, self); + }); + this._models = []; + View.viewCount--; + delete View.views[this.cid]; + return this; + }, + + /** + * utility methods + */ + + getTemplate: function(tmpl) { + if(this.options.template) { + return _.template(this.options.template); + } + return cdb.templates.getTemplate(tmpl); + }, + + show: function() { + this.$el.show(); + }, + + hide: function() { + this.$el.hide(); + }, + + /** + * Listen for an event on another object and triggers on itself, with the same name or a new one + * @method retrigger + * @param ev {String} event who triggers the action + * @param obj {Object} object where the event happens + * @param obj {Object} [optional] name of the retriggered event; + */ + retrigger: function(ev, obj, retrigEvent) { + if(!retrigEvent) { + retrigEvent = ev; + } + var self = this; + obj.bind && obj.bind(ev, function() { + self.trigger(retrigEvent); + }, self) + // add it as related model//object + this.add_related_model(obj); + }, + /** + * Captures an event and prevents the default behaviour and stops it from bubbling + * @method killEvent + * @param event {Event} + */ + killEvent: function(ev) { + if(ev && ev.preventDefault) { + ev.preventDefault(); + }; + if(ev && ev.stopPropagation) { + ev.stopPropagation(); + }; + }, + + /** + * Remove all the tipsy tooltips from the document + * @method cleanTooltips + */ + cleanTooltips: function() { + this.$('.tipsy').remove(); + } + + + + + }, { + viewCount: 0, + views: {}, + + /** + * when a view with events is inherit and you want to add more events + * this helper can be used: + * var MyView = new core.View({ + * events: cdb.core.View.extendEvents({ + * 'click': 'fn' + * }) + * }); + */ + extendEvents: function(newEvents) { + return function() { + return _.extend(newEvents, this.constructor.__super__.events); + }; + }, + + /** + * search for views in a view and check if they are added as subviews + */ + runChecker: function() { + _.each(cdb.core.View.views, function(view) { + _.each(view, function(prop, k) { + if( k !== '_parent' && + view.hasOwnProperty(k) && + prop instanceof cdb.core.View && + view._subviews[prop.cid] === undefined) { + console.log("========="); + console.log("untracked view: "); + console.log(prop.el); + console.log('parent'); + console.log(view.el); + console.log(" "); + } + }); + }); + } + }); + + + /** + * Default widget content view: + * + */ + +cdb.geo.ui.Widget.Content = cdb.core.View.extend({ + + className: 'Widget-body', + + _TEMPLATE: ' ' + + '
              '+ + '
              '+ + '

              <%= title %>

              '+ + '
              '+ + '
              '+ + '
              <%= itemsCount %> items
              '+ + '
              '+ + '
              '+ + '
              ', + + _PLACEHOLDER: ' ' + + '
                ' + + '
              • ' + + '
              ', + + initialize: function() { + this.dataModel = this.options.dataModel; + this.viewModel = this.options.viewModel; + this._initBinds(); + }, + + render: function() { + this.clearSubViews(); + + var template = _.template(this._TEMPLATE); + var data = this.dataModel.getData(); + var isDataEmpty = _.isEmpty(data) || _.size(data) === 0; + this.$el.html( + template({ + title: this.viewModel.get('title'), + itemsCount: !isDataEmpty ? data.length : '-' + }) + ); + + if (isDataEmpty) { + this._addPlaceholder(); + } + + return this; + }, + + _initBinds: function() { + this.dataModel.once('error', this._onFirstLoad, this); + this.dataModel.once('change:data', this._onFirstLoad, this); + this.viewModel.bind('change:sync', this._checkBinds, this); + this.add_related_model(this.dataModel); + this.add_related_model(this.viewModel); + }, + + _onFirstLoad: function() { + this.render(); + this._unbindEvents(); // Remove any old dataModel binding + this._checkBinds(); + }, + + _checkBinds: function() { + var isSync = this.viewModel.get('sync'); + this[ isSync ? '_bindEvents' : '_unbindEvents'](); + }, + + _bindEvents: function() { + this.dataModel.bind('change:data', this.render, this); + }, + + _unbindEvents: function() { + this.dataModel.unbind(null, null, this); + }, + + _addPlaceholder: function() { + var placeholderTemplate = _.template(this._PLACEHOLDER); + this.$('.js-content').append(placeholderTemplate()); + } + +}); + + +cdb.geo.ui.Widget.View = cdb.core.View.extend({ + + className: 'Widget Widget--light', + + options: { + columns_title: [], + sync: true + }, + + initialize: function() { + this.dataModel = this.model; + this.viewModel = new cdb.core.Model({ + title: this.model.get('options').title, + type: this.model.get('options').type, + sync: this.model.get('options').sync, + columns_title: this.model.get('options').columns_title + }); + }, + + render: function() { + this._initViews(); + return this; + }, + + _initViews: function() { + this._loader = new cdb.geo.ui.Widget.Loader({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + this.$el.append(this._loader.render().el); + this.addView(this._loader); + + this._error = new cdb.geo.ui.Widget.Error({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + this._error.bind('refreshData', function() { + console.log("refresh data man!"); + }, this); + this.$el.append(this._error.render().el); + this.addView(this._error); + + var content = this._createContentView(); + this.$el.append(content.render().el); + this.addView(content); + }, + + // Generate and return content view. + // In this case it will be the standard widget content. + _createContentView: function() { + return new cdb.geo.ui.Widget.Content({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + } +}); + + + +cdb.geo.ui.Widget.Collection = Backbone.Collection.extend({ + + model: cdb.geo.ui.Widget.Model + +}); + + +cdb.geo.ui.Widget.Model = cdb.core.Model.extend({ + +}); + + +/** + * Histogram widget view + * + */ + +cdb.geo.ui.Widget.Histogram = {}; + +cdb.geo.ui.Widget.Histogram.View = cdb.geo.ui.Widget.View.extend({ + + _createContentView: function() { + return new cdb.geo.ui.Widget.Histogram.Content({ + viewModel: this.viewModel, + dataModel: this.dataModel + }); + } + +}); + + +cdb.geo.ui.Widget.HistogramModel = cdb.geo.ui.Widget.Model.extend({ + + options: { + page: 0, + per_page: 100 + }, + + defaults: { + data: [], + columns: [] + }, + + url: function() { + // TODO: Change this once the histogram endpoint is ready! + return this.get('dashboardBaseURL') + '/list/' + this.get('id'); + }, + + initialize: function() { + this._data = new Backbone.Collection(this.get('data')); + this._initBinds(); + }, + + _initBinds: function() { + this.bind('change:dashboardBaseURL', function(){ + var self = this; + this.fetch({ + error: function() { + self.trigger('error'); + } + }); + }, this); + }, + + _createUrlOptions: function() { + return _.compact(_(this.options).map( + function(v, k) { + return k + "=" + encodeURIComponent(v); + } + )).join('&'); + }, + + getData: function() { + return this._data; + }, + + getSize: function() { + return this._data.size(); + }, + + getDataSerialized: function() { + return this.get('data'); + }, + + fetch: function(opts) { + this.trigger("loading", this); + return cdb.core.Model.prototype.fetch.call(this,opts); + }, + + parse: function(r) { + this._data.reset(r); + return { + data: r.data + }; + } + +}); + + +/** + * Default widget content view: + * + * + */ + +cdb.geo.ui.Widget.Histogram.Chart = cdb.core.View.extend({ + + defaults: { + duration: 750, + handleWidth: 6, + handleHeight: 23, + handleRadius: 3, + transitionType: 'elastic' + }, + + initialize: function() { + + _.bindAll(this, '_selectBars', '_adjustBrushHandles', '_onBrushMove', '_onBrushStart', '_onMouseMove', '_onMouseEnter', '_onMouseOut'); + + this._setupModel(); + this._setupDimensions(); + }, + + render: function() { + this._generateChart(); + + this._generateHorizontalLines(); + this._generateVerticalLines(); + + this._generateBars(); + + this._generateHandles(); + + this._setupBrush(); + this._generateXAxis(); + + return this; + }, + + _removeBars: function() { + this.chart.selectAll('.Bar').remove(); + }, + + _removeBrush: function() { + this.brush + .clear() + .event(this.chart.select('.Brush')); + this.chart.classed('is-selectable', false); + }, + + reset: function(data) { + this.loadData(data); + this._removeBrush(); + this.model.set({ a: 0, b: this.model.get('data').values.length }); + }, + + _generateVerticalLines: function() { + var range = d3.range(0, this.chartWidth + this.chartWidth / 4, this.chartWidth / 4); + + var lines = this.chart.select('.Lines'); + + lines.append('g') + .attr('class', 'y') + .selectAll('.x') + .data(range.slice(1, range.length - 1)) + .enter().append('svg:line') + .attr('y1', 0) + .attr('x1', function(d) { return d; }) + .attr('y2', this.chartHeight) + .attr('x2', function(d) { return d; }); + }, + + _generateHorizontalLines: function() { + var range = d3.range(0, this.chartHeight + this.chartHeight / 2, this.chartHeight / 2); + + var lines = this.chart.append('g') + .attr('class', 'Lines'); + + lines.append('g') + .attr('class', 'y') + .selectAll('.y') + .data(range) + .enter().append('svg:line') + .attr('class', 'y') + .attr('x1', 0) + .attr('y1', function(d) { return d; }) + .attr('x2', this.chartWidth) + .attr('y2', function(d) { return d; }); + + this.bottomLine = lines + .append('line') + .attr('class', 'l_bottom') + .attr('x1', 0) + .attr('y1', this.chartHeight) + .attr('x2', this.chartWidth - 1) + .attr('y2', this.chartHeight); + }, + + _setupModel: function() { + this.model = new cdb.core.Model({ data: this.options.data }); + this.model.bind('change:a change:b', this._onChangeRange, this); + this.model.bind('change:data', this._onChangeData, this); + this.model.bind('change:dragging', this._onChangeDragging, this); + }, + + _setupDimensions: function() { + var data = this.model.get('data').values; + + this.margin = { top: 0, right: 10, bottom: 20, left: 10 }; + + this.canvasWidth = this.options.width; + this.canvasHeight = this.options.height; + + this.chartWidth = this.canvasWidth - this.margin.left - this.margin.right; + this.chartHeight = this.options.height; + + this._setupScales(); + }, + + _setupScales: function() { + var data = this.model.get('data').values; + this.xScale = d3.scale.linear().domain([0, 100]).range([0, this.chartWidth]); + this.yScale = d3.scale.linear().domain([0, d3.max(data, function(d) { return d; } )]).range([this.chartHeight, 0]); + this.zScale = d3.scale.ordinal().domain(d3.range(data.length)).rangeRoundBands([0, this.chartWidth]); + }, + + _calcBarWidth: function() { + this.barWidth = this.chartWidth / this.model.get('data').values.length; + }, + + _generateChart: function() { + this.chart = d3.select(this.options.el[0]) + .selectAll('.Canvas') + .append('g') + .attr('class', 'Chart') + .attr('opacity', 0) + .attr('transform', 'translate(0, ' + this.options.y + ')'); + + this.chart.classed(this.options.className || '', true); + }, + + hide: function() { + this.chart + .transition() + .duration(150) + .attr('opacity', 0) + .attr('transform', 'translate(0, ' + (this.options.y - 10) + ')'); + }, + + show: function() { + this.chart + .attr('transform', 'translate(0, ' + (this.options.y + 10) + ')') + .transition() + .duration(150) + .attr('opacity', 1) + .attr('transform', 'translate(0, ' + (this.options.y) + ')'); + }, + + move: function() { + this.chart + .transition() + .duration(2500) + .attr('transform', 'translate(0, ' + (this.options.y + 90) + ')'); + }, + + _onBrushStart: function() { + this.chart.classed('is-selectable', true); + }, + + _selectBars: function() { + var self = this; + var extent = this.brush.extent(); + var lo = extent[0]; + var hi = extent[1]; + + this.model.set({ a: this._getLoBarIndex(), b: this._getHiBarIndex() }); + + this.chart.selectAll('.Bar').classed('is-selected', function(d, i) { + var a = Math.floor(i * self.barWidth); + var b = Math.floor(a + self.barWidth); + var LO = Math.floor(self.xScale(lo)); + var HI = Math.floor(self.xScale(hi)); + var isIn = (a > LO && a < HI) || (b > LO && b < HI) || (a <= LO && b >= HI); + return !isIn; + }); + }, + + _onChangeDragging: function() { + this.chart.classed('is-dragging', this.model.get('dragging')); + }, + + _onBrushMove: function() { + this.model.set({ dragging: true }); + this._selectBars(); + this._adjustBrushHandles(); + }, + + _onMouseEnter: function(d) { + }, + + _onMouseOut: function(d) { + var bars = this.chart.selectAll('.Bar'); + bars.classed('is-highlighted', false); + this.trigger('hover', { value: null }); + }, + + _onMouseMove: function(d) { + var x = d3.event.offsetX; + var a = Math.ceil(x / this.barWidth); + var data = this.model.get('data').values; + + var format = d3.format("0,000"); + var bar = this.chart.select('.Bar:nth-child(' + a + ')'); + + if (bar && bar.node() && !bar.classed('is-selected')) { + var left = ((a - 1) * this.barWidth); + if (!this._isDragging()) { + this.trigger('hover', { left: left, value: data[a - 1] }); + } + } else { + this.trigger('hover', { value: null }); + } + + this.chart.selectAll('.Bar') + .classed('is-highlighted', false); + + if (bar && bar.node()) { + bar.classed('is-highlighted', true); + } + }, + + _isDragging: function() { + return this.model.get('dragging'); + }, + + selectRange: function(a, b) { + var data = this.model.get('data').values; + var start = a * (100 / data.length); + var end = b * (100 / data.length); + + this.chart.select('.Brush').transition() + .duration(this.brush.empty() ? 0 : 100) + .call(this.brush.extent([start, end])) + .call(this.brush.event); + }, + + _selectRange: function(start, end) { + this.chart.select('.Brush').transition() + .duration(this.brush.empty() ? 0 : 150) + .call(this.brush.extent([start, end])) + .call(this.brush.event); + }, + + _getLoBarIndex: function() { + var extent = this.brush.extent(); + return Math.round(this.xScale(extent[0]) / this.barWidth); + }, + + _getHiBarIndex: function() { + var extent = this.brush.extent(); + return Math.round(this.xScale(extent[1]) / this.barWidth); + }, + + _getBarIndex: function() { + var x = d3.event.sourceEvent.offsetX - this.margin.left; + return Math.ceil(x / this.barWidth); + }, + + _setupBrush: function() { + var self = this; + + var xScale = this.xScale; + var brush = this.brush = d3.svg.brush().x(this.xScale); + + function onBrushEnd() { + var data = self.model.get('data').values; + var labels = self.model.get('data').labels; + + var a, b; + + self.model.set({ dragging: false }); + + if (brush.empty()) { + self.chart.selectAll('.Bar').classed('is-selected', false); + d3.select(this).call(brush.extent([0, 0])); + } else { + + var loBarIndex = self._getLoBarIndex(); + var hiBarIndex = self._getHiBarIndex(); + + a = loBarIndex * (100 / data.length); + b = hiBarIndex * (100 / data.length); + + if (!d3.event.sourceEvent) { + return; + } + + self._selectRange(a, b); + self.model.set({ a: loBarIndex, b: hiBarIndex }); + self._adjustBrushHandles(); + self._selectBars(); + + self.trigger('on_brush_end', self.model.get('a'), self.model.get('b')); + self.trigger('fliterChanged', labels[self.model.get('a')], labels[self.model.get('b')]); + } + + if (d3.event.sourceEvent && a === undefined && b === undefined) { + var barIndex = self._getBarIndex(); + a = (barIndex - 1) * (100 / data.length); + b = (barIndex) * (100 / data.length); + self.model.set({ a: barIndex - 1, b: barIndex }); + self._selectRange(a, b); + } + } + + var data = this.model.get('data').values; + + this.brush + .on('brushstart', this._onBrushStart) + .on('brush', this._onBrushMove) + .on('brushend', onBrushEnd); + + this.chart.append('g') + .attr('class', 'Brush') + .call(this.brush) + .selectAll('rect') + .attr('y', 0) + .attr('height', this.chartHeight) + .on('mouseenter', this._onMouseEnter) + .on('mouseout', this._onMouseOut) + .on('mousemove', this._onMouseMove); + }, + + _adjustBrushHandles: function() { + var extent = this.brush.extent(); + var lo = extent[0]; + var hi = extent[1]; + + this.leftHandleLine + .attr('x1', this.xScale(lo)) + .attr('x2', this.xScale(lo)); + + this.rightHandleLine + .attr('x1', this.xScale(hi)) + .attr('x2', this.xScale(hi)); + + if (this.options.handles) { + this.leftHandle + .attr('x', this.xScale(lo) - this.defaults.handleWidth / 2); + + this.rightHandle + .attr('x', this.xScale(hi) - this.defaults.handleWidth / 2); + } + }, + + _generateHandle: function() { + var handle = { width: this.defaults.handleWidth, height: this.defaults.handleHeight, radius: this.defaults.handleRadius }; + var yPos = (this.chartHeight / 2) - (this.defaults.handleHeight / 2); + + return this.chart.select('.Handles').append('rect') + .attr('class', 'Handle') + .attr('transform', 'translate(0, ' + yPos + ')') + .attr('width', handle.width) + .attr('height', handle.height) + .attr('rx', handle.radius) + .attr('ry', handle.radius); + }, + + _generateHandleLine: function() { + return this.chart.select('.Handles').append('line') + .attr('class', 'HandleLine') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', 0) + .attr('y2', this.chartHeight); + }, + + _removeHandles: function() { + this.chart.select('.Handles').remove(); + }, + + _generateHandles: function() { + this.chart.append('g').attr('class', 'Handles'); + this.leftHandleLine = this._generateHandleLine(); + this.rightHandleLine = this._generateHandleLine(); + + if (this.options.handles) { + this.leftHandle = this._generateHandle(); + this.rightHandle = this._generateHandle(); + } + }, + + _removeXAxis: function() { + d3.select('.axis').remove(); + }, + + _generateXAxis: function() { + var data = this.model.get('data').labels; + + var format = d3.format('0,000'); + + var xAxis = d3.svg.axis() + .scale(this.zScale) + .orient('bottom') + .innerTickSize(0) + .tickFormat(function(d, i) { + if(i%1==0){ + return data[d] + } + else{ + return '' + } + }); + + this.chart.append('g') + .attr('class', 'axis') + .attr('transform', 'translate(0,' + (this.chartHeight + 5) + ')') + .call(xAxis); + }, + + refreshData: function(data, a, b) { + if (data && data.length > 0) { + this.model.set({ data: data, a: a, b: data.length - 1 }); + } + }, + + loadData: function(data) { + this.model.set({ a: 0, b: 0 }, { silent: true }); + this.model.set('data', data); + this._onChangeData(); + }, + + _onChangeData: function() { + this._removeBrush(); + this._removeBars(); + this._removeHandles(); + + this._setupDimensions(); + this._generateBars(); + this._generateHandles(); + + this._removeXAxis(); + this._generateXAxis(); + + this._setupBrush(); + }, + + _generateBars: function() { + var self = this; + var data = this.model.get('data').values; + + this._calcBarWidth(); + + var bars = this.chart.append('g') + .attr('class', 'Bars') + .selectAll('.Bar') + .data(data); + + bars + .enter() + .append('rect') + .attr('class', 'Bar') + .attr('data', function(d) { return d; }) + .attr('transform', function(d, i) { + return 'translate(' + (i * self.barWidth) + ', 0 )'; + }) + .attr('y', self.chartHeight) + .attr('height', 0) + .attr('width', this.barWidth - 1); + + bars.transition() + .ease(this.defaults.transitionType) + .duration(self.defaults.duration) + .delay(function(d, i) { + return Math.random() * (100 + i * 10); + }) + .attr('height', function(d) { + return d ? self.chartHeight - self.yScale(d) : 0; + }) + .attr('y', function(d) { + return d ? self.yScale(d) : self.chartHeight; + }); + }, + + _onChangeRange: function() { + if (this.model.get('a') === 0 && this.model.get('b') === 0) { + return; + } + this.trigger('range_updated', this.model.get('a'), this.model.get('b')); + }, + + _formatNumber: function(value, unit) { + var format = d3.format("0,000"); + return format(value + unit ? ' ' + unit : ''); + }, + +}); + +cdb.geo.ui.Widget.Histogram.Content = cdb.geo.ui.Widget.Content.extend({ + + defaults: { + chartHeight: 48 + }, + + events: { + 'click .js-clear': '_reset', + 'click .js-zoom': '_zoom' + }, + + _TEMPLATE: ' ' + + '
              '+ + '
              '+ + '

              <%= title %>

              '+ + '
              '+ + '
              '+ + '
              0 NULL ROWS
              '+ + '
              0 MIN
              '+ + '
              0 AVG
              '+ + '
              0 MAX
              '+ + '
              '+ + '
              '+ + '
              '+ + '
              '+ + '
              '+ + '

              '+ + '
              '+ + ' '+ + ' '+ + '
              '+ + '
              '+ + ' ', + + _PLACEHOLDER: ' ' + + '
                ' + + '
              • ' + + '
              • ' + + '
              • ' + + '
              • ' + + '
              ', + + _initViews: function() { + this._generateData(); + this._setupDimensions(); + this._generateCanvas(); + this._renderMainChart(); + this._renderMiniChart(); + }, + + render: function() { + + this.clearSubViews(); + + var template = _.template(this._TEMPLATE); + var data = this.dataModel.getData(); + var isDataEmpty = _.isEmpty(data) || _.size(data) === 0; + + this.$el.html( + template({ + title: this.viewModel.get('title'), + itemsCount: !isDataEmpty ? data.length : '-' + }) + ); + + if (isDataEmpty) { + this._addPlaceholder(); + } else { + this._setupBindings(); + this._initViews(); + } + + return this; + }, + + _renderMainChart: function() { + this.chart = new cdb.geo.ui.Widget.Histogram.Chart(({ + el: this.$('.js-chart'), + y: 0, + handles: true, + width: this.canvasWidth, + height: this.defaults.chartHeight, + data: this.dataModel.get('data') + })); + this.chart.bind('range_updated', this._onRangeUpdated, this); + this.chart.bind('hover', this._onValueHover, this); + this.chart.render().show(); + + this._updateStats(); + }, + + _renderMiniChart: function() { + this.miniChart = new cdb.geo.ui.Widget.Histogram.Chart(({ + className: 'mini', + el: this.$('.js-chart'), + handles: false, + width: this.canvasWidth, + y: 90, + height: 20, + data: this.dataModel.get('data') + })); + + this.miniChart.bind('on_brush_end', this._onMiniRangeUpdated, this); + + this.miniChart.render(); + }, + + _setupBindings: function() { + this.viewModel.bind('change:zoom_enabled', this._onChangeZoomEnabled, this); + this.viewModel.bind('change:total', this._onChangeTotal, this); + this.viewModel.bind('change:max', this._onChangeMax, this); + this.viewModel.bind('change:min', this._onChangeMin, this); + this.viewModel.bind('change:avg', this._onChangeAvg, this); + }, + + _setupDimensions: function() { + this.margin = { top: 0, right: 10, bottom: 20, left: 10 }; + + this.canvasWidth = this.$('.js-chart').width(); + this.canvasHeight = this.defaults.chartHeight + this.margin.top + this.margin.bottom; + }, + + _onValueHover: function(info) { + var $tooltip = this.$(".js-tooltip"); + if (info.value) { + $tooltip.css({ top: 0, left: info.left }); + $tooltip.text(info.value); + $tooltip.show(); + } else { + $tooltip.hide(); + } + }, + + _onMiniRangeUpdated: function(a, b) { + this.viewModel.set({ a: a, b: b }); + var data = this._getData(); + var self = this; + + var refreshData = _.debounce(function() { + self.chart.refreshData(data, a, b); + self._updateStats(); + }, 100); + + refreshData(); + }, + + _onRangeUpdated: function(a, b) { + this.$(".js-filter").animate({ opacity: 1 }, 250); + this.viewModel.set({ a: a, b: b }); + this._updateStats(); + }, + + _onChangeZoomEnabled: function() { + this.$(".js-zoom").toggleClass('is-hidden', !this.viewModel.get('zoom_enabled')); + }, + + _onChangeTotal: function() { + this._animateValue('.js-val', 'total', ' SELECTED'); + }, + + _onChangeMax: function() { + this._animateValue('.js-max', 'max', 'MAX'); + }, + + _onChangeMin: function() { + this._animateValue('.js-min', 'min', 'MIN'); + }, + + _onChangeAvg: function() { + this._animateValue('.js-avg', 'avg', 'AVG'); + }, + + _generateData: function() { + var data = _.map(d3.range(Math.round(Math.random() * 80) + 2), function(d) { + return Math.round(Math.random() * 1000); + }); + + this.dataModel.set('data', data); + }, + + _animateValue: function(className, what, unit) { + var self = this; + var format = d3.format("0,000"); + + var from = this.viewModel.previous(what) || 0; + var to = this.viewModel.get(what); + + if (!to) return; + + $(className).prop('counter', from).stop().animate({ counter: to }, { + duration: 500, + easing: 'swing', + step: function (i) { + $(this).text(format(Math.floor(i)) + ' ' + unit); + } + }); + }, + + _getData: function(full) { + var data = this.dataModel.get('data'); + if (full) { + return data; + } + return data.slice(this.viewModel.get('a'), this.viewModel.get('b')); + }, + + _updateStats: function() { + var data = this._getData(); + var sum = _.reduce(data, function(t, j) { + return j + t; + }); + + var max = d3.max(data); + var avg = Math.round(d3.mean(data)); + var min = d3.min(data); + + this.viewModel.set({ total: sum, min: min, max: max, avg: avg }); + }, + + _zoom: function() { + this._expand(); + this.viewModel.set({ zoom_enabled: false }); + this.chart.loadData(this._getData()); + this.miniChart.selectRange(this.viewModel.get('a'), this.viewModel.get('b')); + this.miniChart.show(); + }, + + _reset: function() { + this._contract(); + this.viewModel.set({ zoom_enabled: true, a: 0, b: 100 }); + this.chart.reset(this._getData()); + this.$(".js-filter").animate({ opacity: 0 }, 0); + this.miniChart.hide(); + }, + + _contract: function() { + this.canvas + .attr('height', this.canvasHeight); + }, + + _expand: function() { + this.canvas + .attr('height', this.canvasHeight + 60); + }, + + _generateCanvas: function() { + this.canvas = d3.select(this.$el.find('.js-chart')[0]) + .attr('width', this.canvasWidth) + .attr('height', this.canvasHeight) + + this.canvas + .append('g') + .attr('class', 'Canvas'); + + this.canvas + .attr('transform', 'translate(10, 0)'); + } +}); diff --git a/examples/vendor/leaflet-hash.js b/examples/vendor/leaflet-hash.js new file mode 100644 index 00000000..a8def32d --- /dev/null +++ b/examples/vendor/leaflet-hash.js @@ -0,0 +1,146 @@ +(function(window) { + var HAS_HASHCHANGE = (function() { + var doc_mode = window.documentMode; + return ('onhashchange' in window) && + (doc_mode === undefined || doc_mode > 7); + })(); + + L.Hash = function(map) { + this.onHashChange = L.Util.bind(this.onHashChange, this); + + if (map) { + this.init(map); + } + }; + + L.Hash.prototype = { + map: null, + lastHash: null, + + parseHash: function(hash) { + if(hash.indexOf('#') == 0) { + hash = hash.substr(1); + } + var args = hash.split("/"); + if (args.length == 3) { + var zoom = parseInt(args[0], 10), + lat = parseFloat(args[1]), + lon = parseFloat(args[2]); + if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { + return false; + } else { + return { + center: new L.LatLng(lat, lon), + zoom: zoom + }; + } + } else { + return false; + } + }, + + formatHash: function(map) { + var center = map.getCenter(), + zoom = map.getZoom(), + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); + + return "#" + [zoom, + center.lat.toFixed(precision), + center.lng.toFixed(precision) + ].join("/"); + }, + + init: function(map) { + this.map = map; + + this.map.on("moveend", this.onMapMove, this); + + // reset the hash + this.lastHash = null; + this.onHashChange(); + + if (!this.isListening) { + this.startListening(); + } + }, + + remove: function() { + this.map = null; + if (this.isListening) { + this.stopListening(); + } + }, + + onMapMove: function(map) { + // bail if we're moving the map (updating from a hash), + // or if the map has no zoom set + + if (this.movingMap || this.map.getZoom() === 0) { + return false; + } + + var hash = this.formatHash(this.map); + if (this.lastHash != hash) { + location.replace(hash); + this.lastHash = hash; + } + }, + + movingMap: false, + update: function() { + var hash = location.hash; + if (hash === this.lastHash) { + // console.info("(no change)"); + return; + } + var parsed = this.parseHash(hash); + if (parsed) { + // console.log("parsed:", parsed.zoom, parsed.center.toString()); + this.movingMap = true; + + this.map.setView(parsed.center, parsed.zoom); + + this.movingMap = false; + } else { + // console.warn("parse error; resetting:", this.map.getCenter(), this.map.getZoom()); + this.onMapMove(this.map); + } + }, + + // defer hash change updates every 100ms + changeDefer: 100, + changeTimeout: null, + onHashChange: function() { + // throttle calls to update() so that they only happen every + // `changeDefer` ms + if (!this.changeTimeout) { + var that = this; + this.changeTimeout = setTimeout(function() { + that.update(); + that.changeTimeout = null; + }, this.changeDefer); + } + }, + + isListening: false, + hashChangeInterval: null, + startListening: function() { + if (HAS_HASHCHANGE) { + L.DomEvent.addListener(window, "hashchange", this.onHashChange); + } else { + clearInterval(this.hashChangeInterval); + this.hashChangeInterval = setInterval(this.onHashChange, 50); + } + this.isListening = true; + }, + + stopListening: function() { + if (HAS_HASHCHANGE) { + L.DomEvent.removeListener(window, "hashchange", this.onHashChange); + } else { + clearInterval(this.hashChangeInterval); + } + this.isListening = false; + } + }; +})(window); \ No newline at end of file diff --git a/examples/vendor/torque.category.css b/examples/vendor/torque.category.css new file mode 100644 index 00000000..9dc1feb5 --- /dev/null +++ b/examples/vendor/torque.category.css @@ -0,0 +1,14 @@ + +.torque-category .axis text { + font: 10px sans-serif; + fill: white; +} +.torque-category .bars rect { + fill: steelblue; +} +.torque-category .axis path, +.torque-category .axis line { + fill: none; + stroke: #FFF; + shape-rendering: crispEdges; +} diff --git a/examples/vendor/torque.category.js b/examples/vendor/torque.category.js new file mode 100644 index 00000000..a9059ec9 --- /dev/null +++ b/examples/vendor/torque.category.js @@ -0,0 +1,140 @@ + +// histogram widget for torque +// d3 based + +function categoryChart() { + var margin = {top: 0, right: 0, bottom: 20, left: 0}, + width = 960, + height = 500; + + var histogram = d3.layout.histogram(), + x = d3.scale.ordinal(), + y = d3.scale.linear(), + xAxis = d3.svg.axis().scale(x).orient("bottom").tickSize(6, 0); + + function chart(selection) { + + var colors = ['#0000b4','#0082ca','#0094ff','#0d4bcf','#0066AE','#074285','#00187B','#285964','#405F83','#416545','#4D7069','#6E9985','#7EBC89','#0283AF','#79BCBF','#99C19E']; + + selection.each(function(data) { + // Compute the histogram. + // data = histogram(data); + + // Update the x-scale. + x .domain(data.map(function(d) { return d[0]; })) + .rangeRoundBands([0, width - margin.left - margin.right], .1); + + // Update the y-scale. + y .domain([]) + .range([height - margin.top - margin.bottom, 0]); + + colorScale = d3.scale.quantize() + .domain([0,data.map(function(d){return d[0]}).length]) + .range(colors); + + // Select the svg element, if it exists. + + var svg = d3.select(this).selectAll("svg") + .attr('class', 'torque-histogram') + .data([data]); + + // Otherwise, create the skeletal chart. + var gEnter = svg.enter().append("svg").append("g"); + gEnter.append("g").attr("class", "bars"); + gEnter.append("g").attr("class", "x axis"); + + // Update the outer dimensions. + svg .attr("width", width) + .attr("height", height); + + // Update the inner dimensions. + var g = svg.select("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Update the bars. + var bar = svg.select(".bars").selectAll(".bar").data(data); + bar.enter().append("rect").attr('class', 'bar'); + bar.exit().remove(); + bar .attr("width", x.rangeBand()) + .attr("x", function(d) { return x(d[0]); }) + .attr("y", function(d) { return y(d[1]); }) + .attr("height", function(d) { return y.range()[0] - y(d[1]); }) + .order(); + + // Update the x-axis. + g.select(".x.axis") + .attr("transform", "translate(0," + y.range()[0] + ")") + .call(xAxis); + }); + } + + chart.margin = function(_) { + if (!arguments.length) return margin; + margin = _; + return chart; + }; + + chart.width = function(_) { + if (!arguments.length) return width; + width = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + // Expose the histogram's value, range and bins method. + d3.rebind(chart, histogram, "value", "range", "bins"); + + // Expose the x-axis' tickFormat method. + d3.rebind(chart, xAxis, "tickFormat"); + + return chart; +} + +torque.widgets = torque.widgets || {} + +/** + * creates an histogram inside the element based on torque data + * @id any d3 valid selector + * @torqueLayer torque layer objecy + * @returns histogram widget object + */ +torque.widgets.category = function(id, torqueLayer, variable) { + + function updateVategories() { + var values = torqueLayer.totalHistogramFor(variable, 0); + var el = d3.select(id) + .datum(values) + + var size = el.node().getBoundingClientRect() + + el.call(histogramChart() + .width(size.width) + .height(size.height) + .bins(10) + .tickFormat(d3.format(".0f")) + ); + }; + + // public API + var h = { + disable: function() { + // torqueLayer.off('change:time', updateHistogram); + // torqueLayer.off('tilesLoaded', updateHistogram); + // torqueLayer.off('tileLoaded', updateHistogram); + torqueLayer.off('histLoaded', updateHistogram); + }, + enable: function() { + // torqueLayer.on('change:time', updateHistogram); + // torqueLayer.on('tilesLoaded', updateHistogram); + // torqueLayer.on('tileLoaded', updateHistogram); + torqueLayer.on('histLoaded', updateHistogram); + } + } + h.enable(); + return h; +} diff --git a/examples/vendor/underscore.js b/examples/vendor/underscore.js new file mode 100644 index 00000000..b29332f9 --- /dev/null +++ b/examples/vendor/underscore.js @@ -0,0 +1,1548 @@ +// Underscore.js 1.8.3 +// http://underscorejs.org +// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind, + nativeCreate = Object.create; + + // Naked function reference for surrogate-prototype-swapping. + var Ctor = function(){}; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.8.3'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return optimizeCb(value, context, argCount); + if (_.isObject(value)) return _.matcher(value); + return _.property(value); + }; + _.iteratee = function(value, context) { + return cb(value, context, Infinity); + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + }; + + // An internal function for creating a new object that inherits from another. + var baseCreate = function(prototype) { + if (!_.isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + }; + + var property = function(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + }; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var getLength = property('length'); + var isArrayLike = function(collection) { + var length = getLength(collection); + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Create a reducing function iterating left or right. + function createReduce(dir) { + // Optimized iterator function as using arguments.length + // in the main function will deoptimize the, see #1991. + function iterator(obj, iteratee, memo, keys, index, length) { + for (; index >= 0 && index < length; index += dir) { + var currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + } + + return function(obj, iteratee, memo, context) { + iteratee = optimizeCb(iteratee, context, 4); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + index = dir > 0 ? 0 : length - 1; + // Determine the initial value if none is provided. + if (arguments.length < 3) { + memo = obj[keys ? keys[index] : index]; + index += dir; + } + return iterator(obj, iteratee, memo, keys, index, length); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = createReduce(-1); + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var key; + if (isArrayLike(obj)) { + key = _.findIndex(obj, predicate, context); + } else { + key = _.findKey(obj, predicate, context); + } + if (key !== void 0 && key !== -1) return obj[key]; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(cb(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given item (using `===`). + // Aliased as `includes` and `include`. + _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return _.indexOf(obj, item, fromIndex) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + var func = isFunc ? method : value[method]; + return func == null ? func : func.apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matcher(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matcher(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var set = isArrayLike(obj) ? obj : _.values(obj); + var length = set.length; + var shuffled = Array(length); + for (var index = 0, rand; index < length; index++) { + rand = _.random(0, index); + if (rand !== index) shuffled[index] = shuffled[rand]; + shuffled[rand] = set[index]; + } + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iteratee(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iteratee, context) { + var result = {}; + iteratee = cb(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (isArrayLike(obj)) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = cb(predicate, context); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + return _.initial(array, array.length - n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return _.rest(array, Math.max(0, array.length - n)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, startIndex) { + var output = [], idx = 0; + for (var i = startIndex || 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { + //flatten current level of array or arguments object + if (!shallow) value = flatten(value, shallow, strict); + var j = 0, len = value.length; + output.length += len; + while (j < len) { + output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!_.contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!_.contains(result, value)) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(arguments, true, true, 1); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + return _.unzip(arguments); + }; + + // Complement of _.zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices + _.unzip = function(array) { + var length = array && _.max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = _.pluck(array, index); + } + return result; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Generator function to create the findIndex and findLastIndex functions + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a predicate test + _.findIndex = createPredicateIndexFinder(1); + _.findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Generator function to create the indexOf and lastIndexOf functions + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), _.isNaN); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); + _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + step = step || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Determines whether to execute a function as a constructor + // or a normal function with the provided arguments + var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (_.isObject(result)) return result; + return self; + }; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + var args = slice.call(arguments, 2); + var bound = function() { + return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var i, length = arguments.length, key; + if (length <= 1) throw new Error('bindAll must be passed function names'); + for (i = 1; i < length; i++) { + key = arguments[i]; + obj[key] = _.bind(obj[key], obj); + } + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = _.partial(_.delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed on and after the Nth call. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed up to (but not including) the Nth call. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + // Object Functions + // ---------------- + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + function collectNonEnumProps(obj, keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve all the property names of an object. + _.allKeys = function(obj) { + if (!_.isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Returns the results of applying the iteratee to each element of the object + // In contrast to _.map it returns an object + _.mapObject = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = _.keys(obj), + length = keys.length, + results = {}, + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = createAssigner(_.allKeys); + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + _.extendOwn = _.assign = createAssigner(_.keys); + + // Returns the first key on an object that passes a predicate test + _.findKey = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = _.keys(obj), key; + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(object, oiteratee, context) { + var result = {}, obj = object, iteratee, keys; + if (obj == null) return result; + if (_.isFunction(oiteratee)) { + keys = _.allKeys(obj); + iteratee = optimizeCb(oiteratee, context); + } else { + keys = flatten(arguments, false, false, 1); + iteratee = function(value, key, obj) { return key in obj; }; + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iteratee, context) { + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + } else { + var keys = _.map(flatten(arguments, false, false, 1), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }; + + // Fill in a given object with default properties. + _.defaults = createAssigner(_.allKeys, true); + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + _.create = function(prototype, props) { + var result = baseCreate(prototype); + if (props) _.extendOwn(result, props); + return result; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Returns whether an object has a given set of `key:value` pairs. + _.isMatch = function(object, attrs) { + var keys = _.keys(attrs), length = keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (_.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; + return _.keys(obj).length === 0; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, + // IE 11 (#1621), and in Safari 8 (#1929). + if (typeof /./ != 'function' && typeof Int8Array != 'object') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + // Predicate-generating functions. Often useful outside of Underscore. + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = property; + + // Generates a function for a given object that returns a given property. + _.propertyOf = function(obj) { + return obj == null ? function(){} : function(key) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + _.matcher = _.matches = function(attrs) { + attrs = _.extendOwn({}, attrs); + return function(obj) { + return _.isMatch(obj, attrs); + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property, fallback) { + var value = object == null ? void 0 : object[property]; + if (value === void 0) { + value = fallback; + } + return _.isFunction(value) ? value.call(object) : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxy for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return '' + this._wrapped; + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}.call(this)); diff --git a/lib/torque/leaflet/torque.js b/lib/torque/leaflet/torque.js index 1c267194..d38f88d1 100644 --- a/lib/torque/leaflet/torque.js +++ b/lib/torque/leaflet/torque.js @@ -25,6 +25,8 @@ L.TorqueLayer = L.CanvasLayer.extend({ if (!torque.isBrowserSupported()) { throw new Error("browser is not supported by torque"); } + + this._histograms = [] options.tileLoader = true; this.key = 0; this.prevRenderedKey = 0; @@ -93,6 +95,10 @@ L.TorqueLayer = L.CanvasLayer.extend({ // don't load tiles that are not being shown if (t.zoom !== self._map.getZoom()) return; self._tileLoaded(t, tileData); + if(Object.keys(tileData.histogram).length > 0){ + self._addTileHistogram(t.x,t.y,t.zoom, tileData.histogram) + } + self._clearTileCaches(); if (tileData) { self.redraw(); @@ -102,7 +108,66 @@ L.TorqueLayer = L.CanvasLayer.extend({ }, this); }, + _invalidateHistograms:function(){ + this._histograms= undefined + }, + _addTileHistogram:function(x, y, zoom, histogram){ + + this._histograms[zoom] = this._histograms[zoom] ? this._histograms[zoom] : [] + this._histograms[zoom][x] = this._histograms[zoom][x] ? this._histograms[zoom][x] : [] + this._histograms[zoom][x][y] = histogram + this.fire('histogramAdded') + }, + histogramForVariable:function(variable){ + histogramParts=[] + Object.keys(this._tiles).forEach(function(t){ + var coord = t.split(":") + if(this._histograms[coord[2]][coord[0]][coord[1]]){ + histogramParts.push(this._histograms[coord[2]][coord[0]][coord[1]][variable]) + } + }.bind(this)) + return this._combineHistograms(histogramParts) + }, + _transfromHistogram:function(hist,zoom){ + var new_hist= { + bins: {}, + bounds: hist.bounds, + zoom : zoom, + x: 0 + } + var zoom_diff = zoom - hist.zoom; + new_hist.x = hist.x >> zoom_diff; + for( var k in hist.bins){ + var idx = (hist.x + k ) >> (zoom_diff - new_hist.x) + new_hist.bins[idx] = new_hist.bins[idx] || 0 + new_hist.bins[idx] += hist.bins[k] + } + return new_hist + }, + _combineHistograms:function(histograms){ + var minZoom = Math.min.apply(null,histograms.map(function(h){return h.zoom})) + var mappedHistograms = histograms.map(function(h){return this._transfromHistogram(h,minZoom)}.bind(this)) + + var combinedHistogram = { + bins:{}, + bounds: [ + Math.min.apply(null,mappedHistograms.map(function(h){return h.bounds[0]})), + Math.max.apply(null,mappedHistograms.map(function(h){return h.bounds[1]})) + ], + zoom: minZoom, + x: 0 + } + + mappedHistograms.forEach(function(histogram){ + Object.keys(histogram.bins).forEach(function(bin){ + combinedHistogram.bins[bin] = combinedHistogram.bins[bin] || 0 + combinedHistogram.bins[bin]+= histogram.bins[bin] + }) + }) + + return combinedHistogram + }, _clearTileCaches: function() { var t, tile; for(t in this._tiles) { @@ -332,6 +397,11 @@ L.TorqueLayer = L.CanvasLayer.extend({ return this.provider.getKeySpan(); }, + setQueryFilter:function(variable, start,end){ + this.provider.setFilter(variable,start,end) + + }, + /** * set the cartocss for the current renderer */ diff --git a/lib/torque/provider/tilejson.js b/lib/torque/provider/tilejson.js index d4b2af7a..3e6ff4e4 100644 --- a/lib/torque/provider/tilejson.js +++ b/lib/torque/provider/tilejson.js @@ -19,6 +19,7 @@ var tileJSON = function (options) { this._ready = false; this._tileQueue = []; + this._filters = {} this.options = options; this.options.coordinates_data_type = this.options.coordinates_data_type || Uint8Array; @@ -49,6 +50,13 @@ this.reload() }, + _setFilter:function(variable, start,end){ + this._filters[variable] = {type:'range', start:start,end:end) + this.reload(); + }, + _removefilter:function(variable){ + this._filters[variable] = undefined + }, /** * return the torque tile encoded in an efficient javascript * structure: @@ -60,16 +68,16 @@ */ createProccessTileWorker:function(){ var workerFunction = "var proccessTile ="+ this.proccessTileSerial.toString() - var wrapper = "; self.onmessage = function(e){var data = JSON.parse(e.data); JSON.stringify(self.postMessage(proccessTile(data.rows,data.coord, data.zoom, data.options)))}" + var wrapper = "; self.onmessage = function(e){var data = JSON.parse(e.data); JSON.stringify(self.postMessage(proccessTile(data.response,data.coord, data.zoom, data.options)))}" var script = workerFunction + wrapper; var blob = new Blob([script], {type: "text/javascript"}) var worker = new Worker(window.URL.createObjectURL(blob)) return worker }, - proccessTile:function(rows,coord,zoom,callback){ + proccessTile:function(response,coord,zoom,callback){ if(typeof(Worker) === "undefined"){ - callback(this.proccessTileSerial(rows,coord,zoom, this.options)) + callback(this.proccessTileSerial(response,coord,zoom, this.options)) } else{ var worker = this.createProccessTileWorker() @@ -79,20 +87,22 @@ } var workerSafeOptions= { - x : new this.options.coordinates_data_type(rows.length), - y : new this.options.coordinates_data_type(rows.length), cumulative: this.options.cumulative, valueDataType: this.options.valueDataType, resolution: this.options.resolution, } - worker.postMessage(JSON.stringify({rows: rows, coord: {x:coord.x,y:coord.y}, zoom:zoom, options: workerSafeOptions})) + worker.postMessage(JSON.stringify({response: response, coord: {x:coord.x,y:coord.y}, zoom:zoom, options: workerSafeOptions})) } }, - proccessTileSerial: function(rows, coord, zoom,options) { + proccessTileSerial: function(response, coord, zoom,options) { var r; - var x = options.x || new options.coordinates_data_type(rows.length); - var y = options.y || new options.coordinates_data_type(rows.length); + var data = JSON.parse(response) + var rows = data.pixels + var histograms = data.histograms + + var x = options.x || new Uint8Array(rows.length); + var y = options.y || new Uint8Array(rows.length); // count number of dates var dates = 0; @@ -189,7 +199,8 @@ timeIndex: timeIndex, renderDataPos: renderDataPos, renderData: renderData, - maxDate: maxDateSlots + maxDate: maxDateSlots, + histogram: histograms }; }, @@ -300,8 +311,8 @@ } torque.net.get( url , function (data) { if (data && data.responseText) { - var rows = JSON.parse(data.responseText); - self.proccessTile(rows.pixels, coord, zoom,callback.bind(self)); + + self.proccessTile(data.responseText, coord, zoom,callback.bind(self)); } else { callback(null); }