diff --git a/app/controllers/explore.coffee b/app/controllers/explore.coffee
index 49a36a6..e9505c5 100644
--- a/app/controllers/explore.coffee
+++ b/app/controllers/explore.coffee
@@ -8,24 +8,25 @@ L = require 'zooniverse/vendor/leaflet/leaflet-src'
moment = require('moment/moment')
animals = require('lib/animals')
+
class Explore extends Controller
className: 'explore'
dateGranularity: 10
- cartoTable: 'serengeti_random'
+ cartoTable: 'serengeti'
layers: []
styles: ['#d32323', '#525b46']
- dateFmt: 'DD MMM YYYY, hh:mm A'
+ dateFmt: 'DD MMM YYYY'
maxCache: 10
cache: []
events:
- 'change select.filter' : 'onSpeciesSelect'
'click input[name="scope"]' : 'setUserScope'
- 'click button.species' : 'onPickSpecies'
+ 'click button.species' : 'showAnimalMenu'
+ 'click div[data-animal]' : 'setSpecies'
'mouseleave .animals' : 'hideAnimalMenu'
- 'click div[data-animal]' : 'onSpeciesSelect'
+ 'change .legend input' : 'toggleLayer'
elements:
'.sign-in' : 'signInContainer'
@@ -59,33 +60,121 @@ class Explore extends Controller
centerOffset: [0, 0]
zoom: 11
className: 'full-screen'
+
+ # Set bounds for the map
+ southWest = new L.LatLng(-3, 34)
+ northEast = new L.LatLng(-2, 36)
+ bounds = new L.LatLngBounds(southWest, northEast)
+ @map.map.setMaxBounds(bounds)
@map.el.appendTo @el.find('.map-container')
+ # Create a custom layer
+ @terrainLayer = L.tileLayer("/tiles/{z}/{x}/{y}.png",
+ minZoom: 7
+ maxZoom: 12
+ attribution: 'Natural Earth (http://www.naturalearthdata.com/)'
+ noWrap: true
+ )
+ @terrainLayer.addTo @map.map
+
# Append div for showing date range
@el.find('.map-container .map').prepend("
")
@dateEl = @el.find('.map-container .map .dates')
+ # Create legend
+ @el.find('.map-container .map').prepend("")
+ @legendEl = @el.find('.map-container .map .legend')
+ @legendEl.append("
")
+ @legendEl.append("")
+
+ # Set date range
+ @startDate = moment('01 Jul 2010, 00:00 PM+02:00')
+ endDate = moment('01 Apr 2012, 00:00 PM+02:00')
+ @interval = endDate.diff(@startDate) / @dateGranularity
+
+ @initCartoDBLayer()
+
@navButtons.first().click()
@onUserSignIn()
-
+
onUserSignIn: =>
@el.toggleClass 'signed-in', !!User.current
+ @my.removeAttr('disabled') # enable 'My Classifications'
+
+ initCartoDBLayer: =>
+ query1 = "WITH hgrid AS (SELECT CDB_HexagonGrid(ST_Expand(CDB_XYZ_Extent({x},{y},{z}),CDB_XYZ_Resolution({z}) * 15),CDB_XYZ_Resolution({z}) * 15 ) as cell) SELECT hgrid.cell as the_geom_webmercator, avg(i.how_many) as prop_count FROM hgrid, serengeti i WHERE i.species = 'zebra' AND ST_Intersects(i.the_geom_webmercator, hgrid.cell) GROUP BY hgrid.cell"
+ query2 = "WITH hgrid AS (SELECT CDB_HexagonGrid(ST_Expand(CDB_XYZ_Extent({x},{y},{z}),CDB_XYZ_Resolution({z}) * 15),CDB_XYZ_Resolution({z}) * 15 ) as cell) SELECT hgrid.cell as the_geom_webmercator, avg(i.how_many) as prop_count FROM hgrid, serengeti i WHERE i.species = 'lionFemale' AND ST_Intersects(i.the_geom_webmercator, hgrid.cell) GROUP BY hgrid.cell"
- # Enable 'My Classifications'
- @my.removeAttr('disabled')
- if User.current
- @requestDateRange()
- # @requestSpecies(0, 1)
+ style1 = '''
+ #serengeti {
+ polygon-opacity:0.6;
+ line-color: #FFF;
+ line-opacity: 0.7;
+ line-width: 1;
+ [prop_count < 1.5] {polygon-fill:#FFF7F3;}
+ [prop_count < 2.0] {polygon-fill:#FDE0DD;}
+ [prop_count < 2.5] {polygon-fill:#FCC5C0;}
+ [prop_count < 3.0] {polygon-fill:#FA9FB5;}
+ [prop_count < 3.5] {polygon-fill:#F768A1;}
+ [prop_count < 4.0] {polygon-fill:#DD3497;}
+ [prop_count < 4.5] {polygon-fill:#AE017E;}
+ [prop_count < 5.0] {polygon-fill:#7A0177;}
+ [prop_count > 5.0] {polygon-fill:#49006A;}
+ }
+ '''
+
+ style2 = '''
+ #serengeti {
+ polygon-opacity:0.4;
+ line-color: #FFF;
+ line-opacity: 0.7;
+ line-width: 1;
+ [prop_count<1.5] {polygon-fill:#F0F9E8;}
+ [prop_count<3.0] {polygon-fill:#BAE4BC;}
+ [prop_count<4.5] {polygon-fill:#7BCCC4;}
+ [prop_count<6.0] {polygon-fill:#43A2CA;}
+ [prop_count>6.0] {polygon-fill:#0868AC;}
+ }
+ '''
+
+ @cartoLayer1 = new L.CartoDBLayer({
+ map: @map.map
+ user_name: 'the-zooniverse'
+ table_name: @cartoTable
+ infowindow: false
+ tile_style: style1
+ query: query1
+ interactivity: false
+ auto_bound: false
+ debug: false
+ })
+ @cartoLayer2 = new L.CartoDBLayer({
+ map: @map.map
+ user_name: 'the-zooniverse'
+ table_name: @cartoTable
+ infowindow: false
+ tile_style: style2
+ query: query2
+ interactivity: false
+ auto_bound: false
+ debug: false
+ })
+ @map.map.addLayer(@cartoLayer1)
+ @map.map.addLayer(@cartoLayer2)
- setUserScope: (e) ->
- @requestDateRange()
- @onSpeciesSelect()
+ setUserScope: -> @setSpecies()
- onPickSpecies: (e) =>
- @speciesIndex = e.target.dataset.index
+ toggleLayer: (e) =>
+ index = e.target.dataset.index
+ if @el.find("#layer#{index}:checked").length is 0
+ @["cartoLayer#{index}"].hide()
+ else
+ @["cartoLayer#{index}"].show()
+
+ showAnimalMenu: (e) =>
+ index = e.target.dataset.index
+ @animalMenu.attr('data-index', index)
@animalMenu.addClass('active')
- position = if @speciesIndex is '1' then 'left' else 'right'
- @animalMenu.attr('data-position', position)
hideAnimalMenu: =>
@animalMenu.removeClass('active')
@@ -95,24 +184,34 @@ class Explore extends Controller
value = ui.value
@requestSpecies(value, value + 1)
- onSpeciesSelect: (e) =>
+ setSpecies: (e) =>
if e?
+ console.log "animal = ", e.target.dataset.animal or e.target.parentElement.dataset.animal
+
+ @hideAnimalMenu()
+
+ target = e.target
species = e.target.innerText
- @["species#{@speciesIndex}"] = e.target.dataset.animal or ''
+ index = target.parentElement.dataset.index or target.parentElement.parentElement.dataset.index
+
+ @["species#{index}"] = target.dataset.animal or target.parentElement.dataset.animal
+
# Swap text in button
- $("button.species[data-index='#{@speciesIndex}']").text(species)
- @hideAnimalMenu()
-
+ $("button.species[data-index='#{index}']").text(species)
+ $("span.animal-name[data-index='#{index}']").text(species)
+ @el.find('.legend').css('opacity', 1)
+
value = @dateSlider.slider('option', 'value')
- @requestSpecies(value, value + 1)
+ @updateCartoQuery(index, @["species#{index}"], value)
updateDateRange: =>
n = @dateSlider.slider('option', 'value')
start = @startDate.clone().add('ms', n * @interval).format(@dateFmt)
end = @startDate.clone().add('ms', (n + 1) * @interval).format(@dateFmt)
- $('.map-container .map .dates').html("#{start} — #{end} (East Africa Time)")
+ $('.map-container .map .dates').html("#{start} — #{end}")
getQueryUrl: (query) ->
+ console.log query
url = encodeURI "http://the-zooniverse.cartodb.com/api/v2/sql?q=#{query}"
return url.replace(/\+/g, '%2B') # Must manually escape plus character (maybe others too)
@@ -150,41 +249,32 @@ class Explore extends Controller
# Methods for querying CartoDB
#
- # Request the minimum and maximum dates of image capture
- requestDateRange: =>
- query = "SELECT MIN(captured_at), MAX(captured_at) FROM #{@cartoTable}"
- if $('input[name="scope"]:checked').val() is 'my'
- query += " WHERE user_id = '#{User.current.id}'"
+ updateCartoQuery: (index, species, startTimeIndex) =>
+ query = "WITH hgrid AS (SELECT CDB_HexagonGrid(ST_Expand(CDB_XYZ_Extent({x},{y},{z}),CDB_XYZ_Resolution({z}) * 15),CDB_XYZ_Resolution({z}) * 15 ) as cell) SELECT hgrid.cell as the_geom_webmercator, avg(i.how_many) as prop_count FROM hgrid, serengeti i WHERE i.species = '#{species}' AND ST_Intersects(i.the_geom_webmercator, hgrid.cell) GROUP BY hgrid.cell"
- url = @getQueryUrl(query)
- $.ajax({url: url, beforeSend: @ajaxStart})
- .done(@getDateRange)
- .then(@ajaxStop)
- # .then(@requestSpecies)
- .fail( (e) -> alert 'Sorry, the query failed')
+ @["cartoLayer#{index}"].setQuery(query)
# Request species counts for all sites between a date interval
- requestSpecies: =>
+ requestSpecies: (n1, n2) =>
start = @startDate.clone()
end = @startDate.clone()
# Get start and end date and update ui
- n = @dateSlider.slider('option', 'value')
- start = @startDate.clone().add('ms', n * @interval).format(@dateFmt)
- end = @startDate.clone().add('ms', (n + 1) * @interval).format(@dateFmt)
- $('.map-container .map .dates').html("#{start} — #{end} (East Africa Time)")
+ start = @startDate.clone().add('ms', n1 * @interval).format(@dateFmt)
+ end = @startDate.clone().add('ms', n2 * @interval).format(@dateFmt)
+ $('.map-container .map .dates').html("#{start} — #{end}")
query = """
- SELECT ST_AsGeoJSON(the_geom) as the_geom, species, AVG(how_many), site_roll_code
- FROM serengeti_random
+ SELECT cartodb_id, ST_AsGeoJSON(the_geom_webmercator) as the_geom_webmercator, species, AVG(how_many), site_roll_code
+ FROM #{@cartoTable}
WHERE (species = '#{@species1}' OR species = '#{@species2}')
"""
if $('input[name="scope"]:checked').val() is 'my'
query += " AND (user_id = '#{User.current.id}') "
query +=
"""
- AND (captured_at BETWEEN '#{start}+02:00' AND '#{end}+02:00')
- GROUP BY the_geom, species, site_roll_code
+ AND (captured_at BETWEEN '#{start}+02:00' AND '#{end}+02:00')
+ GROUP BY the_geom_webmercator, species, site_roll_code
"""
url = @getQueryUrl(query)
@@ -211,18 +301,6 @@ class Explore extends Controller
# Methods for receiving query results from CartoDB
#
- getDateRange: (response) =>
- result = response.rows[0]
-
- @startDate = moment(result.min)
- endDate = moment(result.max)
- @interval = endDate.diff(@startDate) / @dateGranularity
-
- n = @dateSlider.slider('option', 'value')
- start = @startDate.clone().add('ms', n * @interval).format(@dateFmt)
- end = @startDate.clone().add('ms', (n + 1) * @interval).format(@dateFmt)
- $('.map-container .map .dates').html("#{start} — #{end} (East Africa Time)")
-
getSpecies: (rows) =>
cross = crossfilter(rows)
@@ -240,39 +318,26 @@ class Explore extends Controller
@layers = []
for species, index in [species1, species2]
+ # heatmap = []
for row in species
avg = row.avg
[lng, lat] = row.the_geom.coordinates
+ # heatmap.push {lat: lat, lon: lng, value: avg}
# Create two circles over each other
- outerCircle = L.circle([lat, lng], 200 * avg, {
+ circle = L.circle([lat, lng], @getRadius(avg), {
fillColor: @styles[index]
- fillOpacity: 0.01 * Math.exp(avg / 4)
- stroke: false
+ fillOpacity: @getOpacity(avg)
+ color: @styles[index]
+ stroke: true
+ opacity: 0.7
+ weight: 0
})
- # innerCircle = L.circle([lat, lng], 100 * avg, {
- # fillColor: @styles[index]
- # fillOpacity: 0.25
- # stroke: false
- # })
-
- @layers.push outerCircle
- # @layers.push innerCircle
- @map.map.addLayer(outerCircle)
- # @map.map.addLayer(innerCircle)
- # for species, index in [species1, species2]
- # for row in species
- # avg = row.avg
- # [lng, lat] = row.the_geom.coordinates
- #
- # circle = L.circle([lat, lng], 10 * avg, {
- # color: @styles[index]
- # fillColor: @styles[index],
- # fillOpacity: 0.5
- # })
- # @layers.push circle
- # @map.map.addLayer(circle)
-
-
+ @layers.push circle
+ @map.map.addLayer(circle)
+
+ getRadius: (x) -> return 1600 * (-1 + 2 / (1 + Math.exp(-2 * 0.25 * x)))
+ getOpacity: (x) -> return 0.5 * (-1 + 2 / (1 + Math.exp(-2 * x)))
+
module.exports = Explore
diff --git a/app/lib/cartodb-leaflet.js b/app/lib/cartodb-leaflet.js
new file mode 100644
index 0000000..88b3e6b
--- /dev/null
+++ b/app/lib/cartodb-leaflet.js
@@ -0,0 +1,806 @@
+/**
+* @name cartodb-leaflet
+* @version 0.55 [October 31, 2012]
+* @author: Vizzuality.com
+* @fileoverview Author: Vizzuality.com
Licence:
+* Licensed under MIT
+* license.
This library lets you to use CartoDB with Leaflet.
+*
+*/
+
+L = require('zooniverse/vendor/leaflet/leaflet-src');
+
+if (typeof(L.CartoDBLayer) === "undefined") {
+
+ L.CartoDBLayer = L.Class.extend({
+
+ version: "0.55",
+
+ includes: L.Mixin.Events,
+
+ options: {
+ query: "SELECT * FROM {{table_name}}",
+ opacity: 0.99,
+ auto_bound: false,
+ attribution: "CartoDB",
+ debug: false,
+ visible: true,
+ added: false,
+ tiler_domain: "cartodb.com",
+ tiler_port: "80",
+ tiler_protocol: "http",
+ sql_domain: "cartodb.com",
+ sql_port: "80",
+ sql_protocol: "http",
+ extra_params: {},
+ cdn_url: null,
+ subdomains: "abc"
+ },
+
+ /**
+ * Initialize CartoDB Layer
+ * @params {Object}
+ * map - Your Leaflet map
+ * user_name - CartoDB user name
+ * table_name - CartoDB table name
+ * query - If you want to apply any sql sentence to the table...
+ * opacity - If you want to change the opacity of the CartoDB layer
+ * tile_style - If you want to add other style to the layer
+ * interactivity - Get data from the feature clicked ( without any request :) )
+ * featureOver - Callback when user hovers a feature (return mouse event, latlng, position (x & y) and feature data)
+ * featureOut - Callback when user hovers out a feature
+ * featureClick - Callback when user clicks a feature (return mouse/touch event, latlng, position (x & y) and feature data)
+ * attribution - Set the attribution text
+ * debug - Get error messages from the library
+ * auto_bound - Let cartodb auto-bound-zoom in the map (opcional - default = false)
+ *
+ * tiler_protocol - Tiler protocol (opcional - default = 'http')
+ * tiler_domain - Tiler domain (opcional - default = 'cartodb.com')
+ * tiler_port - Tiler port as a string (opcional - default = '80')
+ * sql_protocol - SQL API protocol (opcional - default = 'http')
+ * sql_domain - SQL API domain (opcional - default = 'cartodb.com')
+ * sql_port - SQL API port as a string (opcional - default = '80')
+ * extra_params - In case you want to pass aditional params to cartodb tiler, pass them
+ * as an object
+ * cdn_url - If you want to use a CDN as a proxy set the URL
+ */
+
+ initialize: function (options) {
+ // Set options
+ L.Util.setOptions(this, options);
+
+ // Some checks
+ if (!options.table_name || !options.map) {
+ if (options.debug) {
+ throw('cartodb-leaflet needs at least a CartoDB table name and the Leaflet map object :(');
+ } else { return }
+ }
+
+ // Bounds? CartoDB does it
+ if (options.auto_bound)
+ this.setBounds();
+
+ // Add cartodb logo, yes sir!
+ this._addWadus();
+ },
+
+
+ /**
+ * When Leaflet adds the layer... go!
+ * @params {map}
+ */
+ onAdd: function(map) {
+ this._addLayer();
+ this.fire('added');
+ this.options.added = true;
+ },
+
+
+ /**
+ * When removes the layer, destroy interactivity if exist
+ */
+ onRemove: function(map) {
+ this._remove();
+ this.options.added = false;
+ },
+
+
+ /**
+ * Change opacity of the layer
+ * @params {Integer} New opacity
+ */
+ setOpacity: function(opacity) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (isNaN(opacity) || opacity>1 || opacity<0) {
+ if (this.options.debug) {
+ throw(opacity + ' is not a valid value');
+ } else { return }
+ }
+
+ // Leaflet only accepts 0-0.99... Weird!
+ this.options.opacity = opacity;
+
+ if (this.options.visible) {
+ this.layer.setOpacity(opacity == 1 ? 0.99 : opacity);
+ this.fire('updated');
+ }
+ },
+
+
+ /**
+ * Change query of the tiles
+ * @params {str} New sql for the tiles
+ */
+ setQuery: function(sql) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (!isNaN(sql)) {
+ if (this.options.debug) {
+ throw(sql + ' is not a valid query');
+ } else { return }
+ }
+
+ // Set the new value to the layer options
+ this.options.query = sql;
+ this._update();
+ },
+
+
+ /**
+ * Change style of the tiles
+ * @params {style} New carto for the tiles
+ */
+ setStyle: function(style) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (!isNaN(style)) {
+ if (this.options.debug) {
+ throw(style + ' is not a valid style');
+ } else { return }
+ }
+
+ // Set the new value to the layer options
+ this.options.tile_style = style;
+ this._update();
+ },
+
+
+ /**
+ * Change the query when clicks in a feature
+ * @params {Boolean | String} New sql for the request
+ */
+ setInteractivity: function(value) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (!isNaN(value)) {
+ if (this.options.debug) {
+ throw(value + ' is not a valid setInteractivity value');
+ } else { return }
+ }
+
+ // Set the new value to the layer options
+ this.options.interactivity = value;
+ // Update tiles
+ this._update();
+ },
+
+
+ /**
+ * Change layer index
+ * @params {Integer} New position for the layer
+ */
+ setLayerOrder: function(position) {
+ /*
+ Waiting fot this ticket:
+ https://github.com/CloudMade/Leaflet/issues/505
+ */
+ },
+
+
+ /**
+ * Active or desactive interaction
+ * @params {Boolean} Choose if wants interaction or not
+ */
+ setInteraction: function(bool) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (bool !== false && bool !== true) {
+ if (this.options.debug) {
+ throw(bool + ' is not a valid setInteraction value');
+ } else { return }
+ }
+
+ if (this.interaction) {
+ if (bool) {
+ var self = this;
+ this.interaction.on('on', function(o) {self._bindWaxOnEvents(self.options.map,o)});
+ this.interaction.on('off', function(o) {self._bindWaxOffEvents()});
+ } else {
+ this.interaction.off('on');
+ this.interaction.off('off');
+ }
+ }
+ },
+
+
+ /**
+ * Set a new layer attribution
+ * @params {String} New attribution string
+ */
+ setAttribution: function(attribution) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (!isNaN(attribution)) {
+ if (this.options.debug) {
+ throw(attribution + ' is not a valid attribution');
+ } else { return }
+ }
+
+ // Remove old one
+ this.options.map.attributionControl.removeAttribution(this.options.attribution);
+
+ // Set new attribution in the options
+ this.options.attribution = attribution;
+
+ // Change text
+ this.options.map.attributionControl.addAttribution(this.options.attribution);
+
+ // Change in the layer
+ this.layer.options.attribution = this.options.attribution;
+ this.tilejson.attribution = this.options.attribution;
+
+ this.fire('updated');
+ },
+
+
+ /**
+ * Change multiple options at the same time
+ * @params {Object} New options object
+ */
+ setOptions: function(options) {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (typeof options!= "object" || options.length) {
+ if (this.options.debug) {
+ throw(options + ' options has to be an object');
+ } else { return }
+ }
+
+ // Set options
+ L.Util.setOptions(this, options);
+
+ // Update tiles
+ this._update();
+ },
+
+
+ /**
+ * Returns if the layer is visible or not
+ */
+ isVisible: function() {
+ return this.options.visible
+ },
+
+
+ /**
+ * Returns if the layer belongs to the map
+ */
+ isAdded: function() {
+ return this.options.added
+ },
+
+
+ /**
+ * Hide the CartoDB layer
+ */
+ hide: function() {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (!this.options.visible) {
+ if (this.options.debug) {
+ throw('the layer is already hidden');
+ } else { return }
+ }
+
+ this.layer.setOpacity(0);
+ this.setInteraction(false);
+ this.options.visible = false;
+ this.fire('hidden');
+ },
+
+
+ /**
+ * Show the CartoDB layer
+ */
+ show: function() {
+
+ if (!this.options.added) {
+ if (this.options.debug) {
+ throw('the layer is not still added to the map');
+ } else { return }
+ }
+
+ if (this.options.visible) {
+ if (this.options.debug) {
+ throw('the layer is already shown');
+ } else { return }
+ }
+
+ this.layer.setOpacity(this.options.opacity);
+ this.setInteraction(true);
+ this.options.visible = true;
+ this.fire('shown');
+ },
+
+
+
+ /*
+ * PRIVATE METHODS
+ */
+
+
+ /**
+ * Remove CartoDB layer
+ */
+ _remove: function() {
+ // Unbind interaction
+ this.setInteraction(false);
+
+ // Remove bind loading and load events
+ this.layer
+ .off("loading")
+ .off("load")
+
+ // Remove interacion
+ if (this.interaction)
+ this.interaction.remove();
+
+ // Remove layer
+ this.options.map.removeLayer(this.layer);
+
+ this.fire('removed');
+ },
+
+
+ /**
+ * Update CartoDB layer
+ */
+ _update: function() {
+ // First remove old layer
+ this._remove();
+
+ // Create the new updated one
+ this._addLayer();
+
+ this.fire('updated');
+ },
+
+
+ /**
+ * Zoom to cartodb geometries
+ */
+ setBounds: function(sql) {
+ var self = this
+ , query = "";
+
+ if (sql) {
+ // Custom query
+ query = sql;
+ } else {
+ // Already defined query
+ query = this.options.query;
+ }
+
+ reqwest({
+ url: this._generateCoreUrl("sql") + '/api/v2/sql/?q='+escape('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 ('+ query.replace(/\{\{table_name\}\}/g,this.options.table_name) + ') as subq'),
+ type: 'jsonp',
+ jsonpCallback: 'callback',
+ success: function(result) {
+ if (result.rows[0].maxx!=null) {
+ var coordinates = result.rows[0];
+
+ var lon0 = coordinates.maxx;
+ var lat0 = coordinates.maxy;
+ var lon1 = coordinates.minx;
+ var lat1 = coordinates.miny;
+
+ var minlat = -85.0511;
+ var maxlat = 85.0511;
+ var minlon = -179;
+ var maxlon = 179;
+
+ /* Clamp X to be between min and max (inclusive) */
+ var clampNum = function(x, min, max) {
+ return x < min ? min : x > max ? max : x;
+ }
+
+ lon0 = clampNum(lon0, minlon, maxlon);
+ lon1 = clampNum(lon1, minlon, maxlon);
+ lat0 = clampNum(lat0, minlat, maxlat);
+ lat1 = clampNum(lat1, minlat, maxlat);
+
+ var sw = new L.LatLng(lat0, lon0);
+ var ne = new L.LatLng(lat1, lon1);
+ var bounds = new L.LatLngBounds(sw,ne);
+ self.options.map.fitBounds(bounds);
+ }
+ },
+ error: function(e,msg) {
+ if (this.options.debug) throw('Error getting table bounds: ' + msg);
+ }
+ });
+ },
+
+
+ /**
+ * Add Cartodb logo
+ */
+ _addWadus: function() {
+ if (!document.getElementById('cartodb_logo')) {
+ var cartodb_link = document.createElement("a");
+ cartodb_link.setAttribute('id','cartodb_logo');
+ cartodb_link.setAttribute('style',"position:absolute; bottom:8px; left:8px; display:block; z-index:10000;");
+ cartodb_link.setAttribute('href','http://www.cartodb.com');
+ cartodb_link.setAttribute('target','_blank');
+ cartodb_link.innerHTML = "";
+ this.options.map._container.appendChild(cartodb_link);
+ }
+ },
+
+
+ /**
+ * Add interaction cartodb tiles to the map
+ */
+ _addLayer: function () {
+
+ var self = this;
+
+ // generate the tilejson
+ this.tilejson = this._generateTileJson();
+ this.layer = new wax.leaf.connector(
+ this.tilejson
+ ).on("loading", function() {
+ self.fire("loading", this);
+ }).on("load", function() {
+ self.fire("load", this);
+ });
+
+ // check the tiles
+ this._checkTiles();
+
+ // add the layer to the map
+ this.options.map.addLayer(this.layer,false);
+
+ // add the interaction?
+ if (this.options.interactivity) {
+ this.interaction = wax.leaf.interaction()
+ .map(this.options.map)
+ .tilejson(this.tilejson)
+ .on('on', function(o) {self._bindWaxOnEvents(self.options.map,o)})
+ .on('off', function(o) {self._bindWaxOffEvents()});
+ }
+ },
+
+
+ /**
+ * Bind events for wax interaction
+ * @param {Object} Layer map object
+ * @param {Event} Wax event
+ */
+ _bindWaxOnEvents: function(map,o) {
+ var layer_point = this._findPos(map,o)
+ , latlng = map.layerPointToLatLng(layer_point);
+
+ switch (o.e.type) {
+ case 'mousemove': if (this.options.featureOver) {
+ return this.options.featureOver(o.e,latlng,{x: o.e.clientX, y: o.e.clientY},o.data);
+ } else {
+ if (this.options.debug) throw('featureOver function not defined');
+ }
+ break;
+ case 'click': if (this.options.featureClick) {
+ this.options.featureClick(o.e,latlng,{x: o.e.clientX, y: o.e.clientY},o.data);
+ } else {
+ if (this.options.debug) throw('featureClick function not defined');
+ }
+ break;
+ case 'touchend': if (this.options.featureClick) {
+ this.options.featureClick(o.e,latlng,{x: o.e.clientX, y: o.e.clientY},o.data);
+ } else {
+ if (this.options.debug) throw('featureClick function not defined');
+ }
+ break;
+ default: break;
+ }
+ },
+
+
+ /**
+ * Bind off event for wax interaction
+ */
+ _bindWaxOffEvents: function(){
+ if (this.options.featureOut) {
+ return this.options.featureOut && this.options.featureOut();
+ } else {
+ if (this.options.debug) throw('featureOut function not defined');
+ }
+ },
+
+ /**
+ * Generate tilejson for wax
+ * @return {Object} Options for L.TileLayer
+ */
+ _generateTileJson: function() {
+
+ var urls = this._generateTileUrls();
+ var grids_url = urls.grid_url;
+
+ if (urls.grid_url.indexOf("{s}") != -1) {
+
+ grids_url = [];
+
+ var subdomains = this.options.subdomains;
+
+ if (Object.prototype.toString.call( subdomains ) !== '[object Array]' ) {
+ subdomains.split('');
+ }
+
+ for (var i = 0; i < subdomains.length; i++) {
+ grids_url.push(urls.grid_url.replace(/\{s\}/g, subdomains[i]));
+ }
+ }
+
+ // Build up the tileJSON
+ return {
+ blankImage: '../img/blank_tile.png',
+ tilejson: '1.0.0',
+ scheme: 'xyz',
+ attribution: this.options.attribution,
+ tiles: [urls.tile_url],
+ grids: grids_url,
+ tiles_base: urls.tile_url,
+ grids_base: grids_url,
+ opacity: this.options.opacity,
+ formatter: function(options, data) {
+ return data
+ }
+ };
+ },
+
+
+
+
+ /*
+ * HELPER FUNCTIONS
+ */
+
+
+ /**
+ * Parse URI
+ * @params {String} Tile url
+ * @return {String} URI parsed
+ */
+ _parseUri: function (str) {
+ var o = {
+ strictMode: false,
+ key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
+ q: {
+ name: "queryKey",
+ parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+ },
+ parser: {
+ strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
+ loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+ }
+ },
+ m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
+ uri = {},
+ i = 14;
+
+ while (i--) uri[o.key[i]] = m[i] || "";
+
+ uri[o.q.name] = {};
+ uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
+ if ($1) uri[o.q.name][$1] = $2;
+ });
+ return uri;
+ },
+
+
+ /**
+ * Appends callback onto urls regardless of existing query params
+ * @params {String} Tile url
+ * @params {String} Tile data
+ * @return {String} Tile url parsed
+ */
+ _addUrlData: function (url, data) {
+ url += (this._parseUri(url).query) ? '&' : '?';
+ return url += data;
+ },
+
+
+ /**
+ * Generate the core URL for the tiler
+ * @params {String} Options including tiler_protocol, user_name, tiler_domain and tiler_port
+ */
+ _generateCoreUrl: function(type){
+ //First check if we are using a CDN which in that case we dont need to do all this.
+ if (this.options.cdn_url) {
+ return this.options.cdn_url;
+ }
+
+ if (type == "sql") {
+ return this.options.sql_protocol +
+ "://" + ((this.options.user_name)?this.options.user_name+".":"") +
+ this.options.sql_domain +
+ ((this.options.sql_port != "") ? (":" + this.options.sql_port) : "");
+ } else {
+ return this.options.tiler_protocol +
+ "://" + ((this.options.user_name)?this.options.user_name+".":"") +
+ this.options.tiler_domain +
+ ((this.options.tiler_port != "") ? (":" + this.options.tiler_port) : "");
+ }
+ },
+
+
+ /**
+ * Generate the final tile and grid URLs for the tiler
+ */
+ _generateTileUrls: function() {
+ var core_url = this._generateCoreUrl("tiler")
+ , base_url = core_url + '/tiles/' + this.options.table_name + '/{z}/{x}/{y}'
+ , tile_url = base_url + '.png'
+ , grid_url = base_url + '.grid.json';
+
+ // SQL?
+ if (this.options.query) {
+ var q = encodeURIComponent(this.options.query.replace(/\{\{table_name\}\}/g,this.options.table_name));
+ q = q.replace(/%7Bx%7D/g,"{x}").replace(/%7By%7D/g,"{y}").replace(/%7Bz%7D/g,"{z}");
+ var query = 'sql=' + q
+ tile_url = this._addUrlData(tile_url, query);
+ grid_url = this._addUrlData(grid_url, query);
+ }
+
+ // EXTRA PARAMS?
+ for (_param in this.options.extra_params) {
+ tile_url = this._addUrlData(tile_url, _param+"="+this.options.extra_params[_param]);
+ grid_url = this._addUrlData(grid_url, _param+"="+this.options.extra_params[_param]);
+ }
+
+ // STYLE?
+ if (this.options.tile_style) {
+ var style = 'style=' + encodeURIComponent(this.options.tile_style.replace(/\{\{table_name\}\}/g,this.options.table_name));
+ tile_url = this._addUrlData(tile_url, style);
+ grid_url = this._addUrlData(grid_url, style);
+ }
+
+ // INTERACTIVITY?
+ if (this.options.interactivity) {
+ var interactivity = 'interactivity=' + encodeURIComponent(this.options.interactivity.replace(/ /g,''));
+ tile_url = this._addUrlData(tile_url, interactivity);
+ grid_url = this._addUrlData(grid_url, interactivity);
+ }
+
+ return {
+ core_url: core_url,
+ base_url: base_url,
+ tile_url: tile_url,
+ grid_url: grid_url
+ }
+ },
+
+
+
+ /**
+ * Get the Leaflet Point of the event
+ * @params {Object} Map object
+ * @params {Object} Wax event object
+ */
+ _findPos: function (map,o) {
+ var curleft = curtop = 0;
+ var obj = map._container;
+
+
+ if (obj.offsetParent) {
+ // Modern browsers
+ do {
+ curleft += obj.offsetLeft;
+ curtop += obj.offsetTop;
+ } while (obj = obj.offsetParent);
+ return map.containerPointToLayerPoint(new L.Point((o.e.clientX || o.e.changedTouches[0].clientX) - curleft,(o.e.clientY || o.e.changedTouches[0].clientY) - curtop))
+ } else {
+ // IE
+ return map.mouseEventToLayerPoint(o.e)
+ }
+ },
+
+ /**
+ * Check the tiles
+ */
+ _checkTiles: function() {
+ var xyz = {
+ z: 4,
+ x: 6,
+ y: 6
+ },
+ self = this,
+ img = new Image(),
+ urls = this._generateTileUrls()
+
+ // Choose a x-y-z for the check tile - grid
+ urls.tile_url = urls.tile_url.replace(/\{z\}/g, xyz.z).replace(/\{x\}/g, xyz.x).replace(/\{y\}/g, xyz.y);
+ urls.grid_url = urls.grid_url.replace(/\{z\}/g, xyz.z).replace(/\{x\}/g, xyz.x).replace(/\{y\}/g, xyz.y);
+
+ reqwest({
+ method: "get",
+ url: urls.grid_url.replace(/\{s\}/g, "a"),
+ type: 'jsonp',
+ jsonpCallback: 'callback',
+ jsonpCallbackName: 'grid',
+ success: function() {
+ clearTimeout(timeout)
+ },
+ error: function(error, msg) {
+ if (self.interaction) self.interaction.remove();
+
+ if (self.options.debug) throw ('There is an error in your query or your interaction parameter');
+
+ self.fire("layererror", msg);
+ }
+ });
+
+ // Hacky for reqwest, due to timeout doesn't work very well
+ var timeout = setTimeout(function() {
+ clearTimeout(timeout);
+
+ if (self.options.debug) throw ('There is an error in your query or your interaction parameter');
+
+ self.fire("layererror", "There is a problem in your SQL or interaction parameter");
+ }, 2000);
+ }
+
+ });
+}
\ No newline at end of file
diff --git a/app/lib/setup.coffee b/app/lib/setup.coffee
index ccf72b5..1be8c75 100644
--- a/app/lib/setup.coffee
+++ b/app/lib/setup.coffee
@@ -5,4 +5,7 @@ require 'jqueryify'
require 'spine'
# Here so that it is included after jQuery
-require 'lib/jquery-ui-1.9.2.custom'
\ No newline at end of file
+require 'lib/jquery-ui-1.9.2.custom'
+require 'lib/heatmap-leaflet'
+require 'lib/wax.leaf'
+require 'lib/cartodb-leaflet'
\ No newline at end of file
diff --git a/app/lib/wax.leaf.js b/app/lib/wax.leaf.js
new file mode 100755
index 0000000..a2db1f3
--- /dev/null
+++ b/app/lib/wax.leaf.js
@@ -0,0 +1,3332 @@
+/* wax - 6.4.0 - v6.0.4-28-g4d63117 */
+
+!function (name, context, definition) {
+ if (typeof module !== 'undefined') module.exports = definition(name, context);
+ else if (typeof define === 'function' && typeof define.amd === 'object') define(definition);
+ else context[name] = definition(name, context);
+}('bean', this, function (name, context) {
+ var win = window
+ , old = context[name]
+ , overOut = /over|out/
+ , namespaceRegex = /[^\.]*(?=\..*)\.|.*/
+ , nameRegex = /\..*/
+ , addEvent = 'addEventListener'
+ , attachEvent = 'attachEvent'
+ , removeEvent = 'removeEventListener'
+ , detachEvent = 'detachEvent'
+ , doc = document || {}
+ , root = doc.documentElement || {}
+ , W3C_MODEL = root[addEvent]
+ , eventSupport = W3C_MODEL ? addEvent : attachEvent
+ , slice = Array.prototype.slice
+ , mouseTypeRegex = /click|mouse(?!(.*wheel|scroll))|menu|drag|drop/i
+ , mouseWheelTypeRegex = /mouse.*(wheel|scroll)/i
+ , textTypeRegex = /^text/i
+ , touchTypeRegex = /^touch|^gesture/i
+ , ONE = { one: 1 } // singleton for quick matching making add() do one()
+
+ , nativeEvents = (function (hash, events, i) {
+ for (i = 0; i < events.length; i++)
+ hash[events[i]] = 1
+ return hash
+ })({}, (
+ 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
+ 'mousewheel mousemultiwheel DOMMouseScroll ' + // mouse wheel
+ 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
+ 'keydown keypress keyup ' + // keyboard
+ 'orientationchange ' + // mobile
+ 'focus blur change reset select submit ' + // form elements
+ 'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window
+ 'error abort scroll ' + // misc
+ (W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
+ // that doesn't actually exist, so make sure we only do these on newer browsers
+ 'show ' + // mouse buttons
+ 'input invalid ' + // form elements
+ 'touchstart touchmove touchend touchcancel ' + // touch
+ 'gesturestart gesturechange gestureend ' + // gesture
+ 'message readystatechange pageshow pagehide popstate ' + // window
+ 'hashchange offline online ' + // window
+ 'afterprint beforeprint ' + // printing
+ 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
+ 'loadstart progress suspend emptied stalled loadmetadata ' + // media
+ 'loadeddata canplay canplaythrough playing waiting seeking ' + // media
+ 'seeked ended durationchange timeupdate play pause ratechange ' + // media
+ 'volumechange cuechange ' + // media
+ 'checking noupdate downloading cached updateready obsolete ' + // appcache
+ '' : '')
+ ).split(' ')
+ )
+
+ , customEvents = (function () {
+ function isDescendant(parent, node) {
+ while ((node = node.parentNode) !== null) {
+ if (node === parent) return true
+ }
+ return false
+ }
+
+ function check(event) {
+ var related = event.relatedTarget
+ if (!related) return related === null
+ return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related))
+ }
+
+ return {
+ mouseenter: { base: 'mouseover', condition: check }
+ , mouseleave: { base: 'mouseout', condition: check }
+ , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
+ }
+ })()
+
+ , fixEvent = (function () {
+ var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ')
+ , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' '))
+ , mouseWheelProps = mouseProps.concat('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ axis'.split(' ')) // 'axis' is FF specific
+ , keyProps = commonProps.concat('char charCode key keyCode keyIdentifier keyLocation'.split(' '))
+ , textProps = commonProps.concat(['data'])
+ , touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
+ , preventDefault = 'preventDefault'
+ , createPreventDefault = function (event) {
+ return function () {
+ if (event[preventDefault])
+ event[preventDefault]()
+ else
+ event.returnValue = false
+ }
+ }
+ , stopPropagation = 'stopPropagation'
+ , createStopPropagation = function (event) {
+ return function () {
+ if (event[stopPropagation])
+ event[stopPropagation]()
+ else
+ event.cancelBubble = true
+ }
+ }
+ , createStop = function (synEvent) {
+ return function () {
+ synEvent[preventDefault]()
+ synEvent[stopPropagation]()
+ synEvent.stopped = true
+ }
+ }
+ , copyProps = function (event, result, props) {
+ var i, p
+ for (i = props.length; i--;) {
+ p = props[i]
+ if (!(p in result) && p in event) result[p] = event[p]
+ }
+ }
+
+ return function (event, isNative) {
+ var result = { originalEvent: event, isNative: isNative }
+ if (!event)
+ return result
+
+ var props
+ , type = event.type
+ , target = event.target || event.srcElement
+
+ result[preventDefault] = createPreventDefault(event)
+ result[stopPropagation] = createStopPropagation(event)
+ result.stop = createStop(result)
+ result.target = target && target.nodeType === 3 ? target.parentNode : target
+
+ if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive
+ if (type.indexOf('key') !== -1) {
+ props = keyProps
+ result.keyCode = event.which || event.keyCode
+ } else if (mouseTypeRegex.test(type)) {
+ props = mouseProps
+ result.rightClick = event.which === 3 || event.button === 2
+ result.pos = { x: 0, y: 0 }
+ if (event.pageX || event.pageY) {
+ result.clientX = event.pageX
+ result.clientY = event.pageY
+ } else if (event.clientX || event.clientY) {
+ result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
+ result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
+ }
+ if (overOut.test(type))
+ result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element']
+ } else if (touchTypeRegex.test(type)) {
+ props = touchProps
+ } else if (mouseWheelTypeRegex.test(type)) {
+ props = mouseWheelProps
+ } else if (textTypeRegex.test(type)) {
+ props = textProps
+ }
+ copyProps(event, result, props || commonProps)
+ }
+ return result
+ }
+ })()
+
+ // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
+ , targetElement = function (element, isNative) {
+ return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
+ }
+
+ // we use one of these per listener, of any type
+ , RegEntry = (function () {
+ function entry(element, type, handler, original, namespaces) {
+ this.element = element
+ this.type = type
+ this.handler = handler
+ this.original = original
+ this.namespaces = namespaces
+ this.custom = customEvents[type]
+ this.isNative = nativeEvents[type] && element[eventSupport]
+ this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange'
+ this.customType = !W3C_MODEL && !this.isNative && type
+ this.target = targetElement(element, this.isNative)
+ this.eventSupport = this.target[eventSupport]
+ }
+
+ entry.prototype = {
+ // given a list of namespaces, is our entry in any of them?
+ inNamespaces: function (checkNamespaces) {
+ var i, j
+ if (!checkNamespaces)
+ return true
+ if (!this.namespaces)
+ return false
+ for (i = checkNamespaces.length; i--;) {
+ for (j = this.namespaces.length; j--;) {
+ if (checkNamespaces[i] === this.namespaces[j])
+ return true
+ }
+ }
+ return false
+ }
+
+ // match by element, original fn (opt), handler fn (opt)
+ , matches: function (checkElement, checkOriginal, checkHandler) {
+ return this.element === checkElement &&
+ (!checkOriginal || this.original === checkOriginal) &&
+ (!checkHandler || this.handler === checkHandler)
+ }
+ }
+
+ return entry
+ })()
+
+ , registry = (function () {
+ // our map stores arrays by event type, just because it's better than storing
+ // everything in a single array. uses '$' as a prefix for the keys for safety
+ var map = {}
+
+ // generic functional search of our registry for matching listeners,
+ // `fn` returns false to break out of the loop
+ , forAll = function (element, type, original, handler, fn) {
+ if (!type || type === '*') {
+ // search the whole registry
+ for (var t in map) {
+ if (t.charAt(0) === '$')
+ forAll(element, t.substr(1), original, handler, fn)
+ }
+ } else {
+ var i = 0, l, list = map['$' + type], all = element === '*'
+ if (!list)
+ return
+ for (l = list.length; i < l; i++) {
+ if (all || list[i].matches(element, original, handler))
+ if (!fn(list[i], list, i, type))
+ return
+ }
+ }
+ }
+
+ , has = function (element, type, original) {
+ // we're not using forAll here simply because it's a bit slower and this
+ // needs to be fast
+ var i, list = map['$' + type]
+ if (list) {
+ for (i = list.length; i--;) {
+ if (list[i].matches(element, original, null))
+ return true
+ }
+ }
+ return false
+ }
+
+ , get = function (element, type, original) {
+ var entries = []
+ forAll(element, type, original, null, function (entry) { return entries.push(entry) })
+ return entries
+ }
+
+ , put = function (entry) {
+ (map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry)
+ return entry
+ }
+
+ , del = function (entry) {
+ forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) {
+ list.splice(i, 1)
+ if (list.length === 0)
+ delete map['$' + entry.type]
+ return false
+ })
+ }
+
+ // dump all entries, used for onunload
+ , entries = function () {
+ var t, entries = []
+ for (t in map) {
+ if (t.charAt(0) === '$')
+ entries = entries.concat(map[t])
+ }
+ return entries
+ }
+
+ return { has: has, get: get, put: put, del: del, entries: entries }
+ })()
+
+ // add and remove listeners to DOM elements
+ , listener = W3C_MODEL ? function (element, type, fn, add) {
+ element[add ? addEvent : removeEvent](type, fn, false)
+ } : function (element, type, fn, add, custom) {
+ if (custom && add && element['_on' + custom] === null)
+ element['_on' + custom] = 0
+ element[add ? attachEvent : detachEvent]('on' + type, fn)
+ }
+
+ , nativeHandler = function (element, fn, args) {
+ return function (event) {
+ event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true)
+ return fn.apply(element, [event].concat(args))
+ }
+ }
+
+ , customHandler = function (element, fn, type, condition, args, isNative) {
+ return function (event) {
+ if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) {
+ if (event)
+ event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative)
+ fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args))
+ }
+ }
+ }
+
+ , once = function (rm, element, type, fn, originalFn) {
+ // wrap the handler in a handler that does a remove as well
+ return function () {
+ rm(element, type, originalFn)
+ fn.apply(this, arguments)
+ }
+ }
+
+ , removeListener = function (element, orgType, handler, namespaces) {
+ var i, l, entry
+ , type = (orgType && orgType.replace(nameRegex, ''))
+ , handlers = registry.get(element, type, handler)
+
+ for (i = 0, l = handlers.length; i < l; i++) {
+ if (handlers[i].inNamespaces(namespaces)) {
+ if ((entry = handlers[i]).eventSupport)
+ listener(entry.target, entry.eventType, entry.handler, false, entry.type)
+ // TODO: this is problematic, we have a registry.get() and registry.del() that
+ // both do registry searches so we waste cycles doing this. Needs to be rolled into
+ // a single registry.forAll(fn) that removes while finding, but the catch is that
+ // we'll be splicing the arrays that we're iterating over. Needs extra tests to
+ // make sure we don't screw it up. @rvagg
+ registry.del(entry)
+ }
+ }
+ }
+
+ , addListener = function (element, orgType, fn, originalFn, args) {
+ var entry
+ , type = orgType.replace(nameRegex, '')
+ , namespaces = orgType.replace(namespaceRegex, '').split('.')
+
+ if (registry.has(element, type, fn))
+ return element // no dupe
+ if (type === 'unload')
+ fn = once(removeListener, element, type, fn, originalFn) // self clean-up
+ if (customEvents[type]) {
+ if (customEvents[type].condition)
+ fn = customHandler(element, fn, type, customEvents[type].condition, true)
+ type = customEvents[type].base || type
+ }
+ entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces))
+ entry.handler = entry.isNative ?
+ nativeHandler(element, entry.handler, args) :
+ customHandler(element, entry.handler, type, false, args, false)
+ if (entry.eventSupport)
+ listener(entry.target, entry.eventType, entry.handler, true, entry.customType)
+ }
+
+ , del = function (selector, fn, $) {
+ return function (e) {
+ var target, i, array = typeof selector === 'string' ? $(selector, this) : selector
+ for (target = e.target; target && target !== this; target = target.parentNode) {
+ for (i = array.length; i--;) {
+ if (array[i] === target) {
+ return fn.apply(target, arguments)
+ }
+ }
+ }
+ }
+ }
+
+ , remove = function (element, typeSpec, fn) {
+ var k, m, type, namespaces, i
+ , rm = removeListener
+ , isString = typeSpec && typeof typeSpec === 'string'
+
+ if (isString && typeSpec.indexOf(' ') > 0) {
+ // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3')
+ typeSpec = typeSpec.split(' ')
+ for (i = typeSpec.length; i--;)
+ remove(element, typeSpec[i], fn)
+ return element
+ }
+ type = isString && typeSpec.replace(nameRegex, '')
+ if (type && customEvents[type])
+ type = customEvents[type].type
+ if (!typeSpec || isString) {
+ // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3)
+ if (namespaces = isString && typeSpec.replace(namespaceRegex, ''))
+ namespaces = namespaces.split('.')
+ rm(element, type, fn, namespaces)
+ } else if (typeof typeSpec === 'function') {
+ // remove(el, fn)
+ rm(element, null, typeSpec)
+ } else {
+ // remove(el, { t1: fn1, t2, fn2 })
+ for (k in typeSpec) {
+ if (typeSpec.hasOwnProperty(k))
+ remove(element, k, typeSpec[k])
+ }
+ }
+ return element
+ }
+
+ , add = function (element, events, fn, delfn, $) {
+ var type, types, i, args
+ , originalFn = fn
+ , isDel = fn && typeof fn === 'string'
+
+ if (events && !fn && typeof events === 'object') {
+ for (type in events) {
+ if (events.hasOwnProperty(type))
+ add.apply(this, [ element, type, events[type] ])
+ }
+ } else {
+ args = arguments.length > 3 ? slice.call(arguments, 3) : []
+ types = (isDel ? fn : events).split(' ')
+ isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1))
+ // special case for one()
+ this === ONE && (fn = once(remove, element, events, fn, originalFn))
+ for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args)
+ }
+ return element
+ }
+
+ , one = function () {
+ return add.apply(ONE, arguments)
+ }
+
+ , fireListener = W3C_MODEL ? function (isNative, type, element) {
+ var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
+ evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
+ element.dispatchEvent(evt)
+ } : function (isNative, type, element) {
+ element = targetElement(element, isNative)
+ // if not-native then we're using onpropertychange so we just increment a custom property
+ isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
+ }
+
+ , fire = function (element, type, args) {
+ var i, j, l, names, handlers
+ , types = type.split(' ')
+
+ for (i = types.length; i--;) {
+ type = types[i].replace(nameRegex, '')
+ if (names = types[i].replace(namespaceRegex, ''))
+ names = names.split('.')
+ if (!names && !args && element[eventSupport]) {
+ fireListener(nativeEvents[type], type, element)
+ } else {
+ // non-native event, either because of a namespace, arguments or a non DOM element
+ // iterate over all listeners and manually 'fire'
+ handlers = registry.get(element, type)
+ args = [false].concat(args)
+ for (j = 0, l = handlers.length; j < l; j++) {
+ if (handlers[j].inNamespaces(names))
+ handlers[j].handler.apply(element, args)
+ }
+ }
+ }
+ return element
+ }
+
+ , clone = function (element, from, type) {
+ var i = 0
+ , handlers = registry.get(from, type)
+ , l = handlers.length
+
+ for (;i < l; i++)
+ handlers[i].original && add(element, handlers[i].type, handlers[i].original)
+ return element
+ }
+
+ , bean = {
+ add: add
+ , one: one
+ , remove: remove
+ , clone: clone
+ , fire: fire
+ , noConflict: function () {
+ context[name] = old
+ return this
+ }
+ }
+
+ if (win[attachEvent]) {
+ // for IE, clean up on unload to avoid leaks
+ var cleanup = function () {
+ var i, entries = registry.entries()
+ for (i in entries) {
+ if (entries[i].type && entries[i].type !== 'unload')
+ remove(entries[i].element, entries[i].type)
+ }
+ win[detachEvent]('onunload', cleanup)
+ win.CollectGarbage && win.CollectGarbage()
+ }
+ win[attachEvent]('onunload', cleanup)
+ }
+
+ return bean
+})
+// Copyright Google Inc.
+// Licensed under the Apache Licence Version 2.0
+// Autogenerated at Tue Oct 11 13:36:46 EDT 2011
+// @provides html4
+var html4 = {};
+html4.atype = {
+ NONE: 0,
+ URI: 1,
+ URI_FRAGMENT: 11,
+ SCRIPT: 2,
+ STYLE: 3,
+ ID: 4,
+ IDREF: 5,
+ IDREFS: 6,
+ GLOBAL_NAME: 7,
+ LOCAL_NAME: 8,
+ CLASSES: 9,
+ FRAME_TARGET: 10
+};
+html4.ATTRIBS = {
+ '*::class': 9,
+ '*::dir': 0,
+ '*::id': 4,
+ '*::lang': 0,
+ '*::onclick': 2,
+ '*::ondblclick': 2,
+ '*::onkeydown': 2,
+ '*::onkeypress': 2,
+ '*::onkeyup': 2,
+ '*::onload': 2,
+ '*::onmousedown': 2,
+ '*::onmousemove': 2,
+ '*::onmouseout': 2,
+ '*::onmouseover': 2,
+ '*::onmouseup': 2,
+ '*::style': 3,
+ '*::title': 0,
+ 'a::accesskey': 0,
+ 'a::coords': 0,
+ 'a::href': 1,
+ 'a::hreflang': 0,
+ 'a::name': 7,
+ 'a::onblur': 2,
+ 'a::onfocus': 2,
+ 'a::rel': 0,
+ 'a::rev': 0,
+ 'a::shape': 0,
+ 'a::tabindex': 0,
+ 'a::target': 10,
+ 'a::type': 0,
+ 'area::accesskey': 0,
+ 'area::alt': 0,
+ 'area::coords': 0,
+ 'area::href': 1,
+ 'area::nohref': 0,
+ 'area::onblur': 2,
+ 'area::onfocus': 2,
+ 'area::shape': 0,
+ 'area::tabindex': 0,
+ 'area::target': 10,
+ 'bdo::dir': 0,
+ 'blockquote::cite': 1,
+ 'br::clear': 0,
+ 'button::accesskey': 0,
+ 'button::disabled': 0,
+ 'button::name': 8,
+ 'button::onblur': 2,
+ 'button::onfocus': 2,
+ 'button::tabindex': 0,
+ 'button::type': 0,
+ 'button::value': 0,
+ 'canvas::height': 0,
+ 'canvas::width': 0,
+ 'caption::align': 0,
+ 'col::align': 0,
+ 'col::char': 0,
+ 'col::charoff': 0,
+ 'col::span': 0,
+ 'col::valign': 0,
+ 'col::width': 0,
+ 'colgroup::align': 0,
+ 'colgroup::char': 0,
+ 'colgroup::charoff': 0,
+ 'colgroup::span': 0,
+ 'colgroup::valign': 0,
+ 'colgroup::width': 0,
+ 'del::cite': 1,
+ 'del::datetime': 0,
+ 'dir::compact': 0,
+ 'div::align': 0,
+ 'dl::compact': 0,
+ 'font::color': 0,
+ 'font::face': 0,
+ 'font::size': 0,
+ 'form::accept': 0,
+ 'form::action': 1,
+ 'form::autocomplete': 0,
+ 'form::enctype': 0,
+ 'form::method': 0,
+ 'form::name': 7,
+ 'form::onreset': 2,
+ 'form::onsubmit': 2,
+ 'form::target': 10,
+ 'h1::align': 0,
+ 'h2::align': 0,
+ 'h3::align': 0,
+ 'h4::align': 0,
+ 'h5::align': 0,
+ 'h6::align': 0,
+ 'hr::align': 0,
+ 'hr::noshade': 0,
+ 'hr::size': 0,
+ 'hr::width': 0,
+ 'iframe::align': 0,
+ 'iframe::frameborder': 0,
+ 'iframe::height': 0,
+ 'iframe::marginheight': 0,
+ 'iframe::marginwidth': 0,
+ 'iframe::width': 0,
+ 'img::align': 0,
+ 'img::alt': 0,
+ 'img::border': 0,
+ 'img::height': 0,
+ 'img::hspace': 0,
+ 'img::ismap': 0,
+ 'img::name': 7,
+ 'img::src': 1,
+ 'img::usemap': 11,
+ 'img::vspace': 0,
+ 'img::width': 0,
+ 'input::accept': 0,
+ 'input::accesskey': 0,
+ 'input::align': 0,
+ 'input::alt': 0,
+ 'input::autocomplete': 0,
+ 'input::checked': 0,
+ 'input::disabled': 0,
+ 'input::ismap': 0,
+ 'input::maxlength': 0,
+ 'input::name': 8,
+ 'input::onblur': 2,
+ 'input::onchange': 2,
+ 'input::onfocus': 2,
+ 'input::onselect': 2,
+ 'input::readonly': 0,
+ 'input::size': 0,
+ 'input::src': 1,
+ 'input::tabindex': 0,
+ 'input::type': 0,
+ 'input::usemap': 11,
+ 'input::value': 0,
+ 'ins::cite': 1,
+ 'ins::datetime': 0,
+ 'label::accesskey': 0,
+ 'label::for': 5,
+ 'label::onblur': 2,
+ 'label::onfocus': 2,
+ 'legend::accesskey': 0,
+ 'legend::align': 0,
+ 'li::type': 0,
+ 'li::value': 0,
+ 'map::name': 7,
+ 'menu::compact': 0,
+ 'ol::compact': 0,
+ 'ol::start': 0,
+ 'ol::type': 0,
+ 'optgroup::disabled': 0,
+ 'optgroup::label': 0,
+ 'option::disabled': 0,
+ 'option::label': 0,
+ 'option::selected': 0,
+ 'option::value': 0,
+ 'p::align': 0,
+ 'pre::width': 0,
+ 'q::cite': 1,
+ 'select::disabled': 0,
+ 'select::multiple': 0,
+ 'select::name': 8,
+ 'select::onblur': 2,
+ 'select::onchange': 2,
+ 'select::onfocus': 2,
+ 'select::size': 0,
+ 'select::tabindex': 0,
+ 'table::align': 0,
+ 'table::bgcolor': 0,
+ 'table::border': 0,
+ 'table::cellpadding': 0,
+ 'table::cellspacing': 0,
+ 'table::frame': 0,
+ 'table::rules': 0,
+ 'table::summary': 0,
+ 'table::width': 0,
+ 'tbody::align': 0,
+ 'tbody::char': 0,
+ 'tbody::charoff': 0,
+ 'tbody::valign': 0,
+ 'td::abbr': 0,
+ 'td::align': 0,
+ 'td::axis': 0,
+ 'td::bgcolor': 0,
+ 'td::char': 0,
+ 'td::charoff': 0,
+ 'td::colspan': 0,
+ 'td::headers': 6,
+ 'td::height': 0,
+ 'td::nowrap': 0,
+ 'td::rowspan': 0,
+ 'td::scope': 0,
+ 'td::valign': 0,
+ 'td::width': 0,
+ 'textarea::accesskey': 0,
+ 'textarea::cols': 0,
+ 'textarea::disabled': 0,
+ 'textarea::name': 8,
+ 'textarea::onblur': 2,
+ 'textarea::onchange': 2,
+ 'textarea::onfocus': 2,
+ 'textarea::onselect': 2,
+ 'textarea::readonly': 0,
+ 'textarea::rows': 0,
+ 'textarea::tabindex': 0,
+ 'tfoot::align': 0,
+ 'tfoot::char': 0,
+ 'tfoot::charoff': 0,
+ 'tfoot::valign': 0,
+ 'th::abbr': 0,
+ 'th::align': 0,
+ 'th::axis': 0,
+ 'th::bgcolor': 0,
+ 'th::char': 0,
+ 'th::charoff': 0,
+ 'th::colspan': 0,
+ 'th::headers': 6,
+ 'th::height': 0,
+ 'th::nowrap': 0,
+ 'th::rowspan': 0,
+ 'th::scope': 0,
+ 'th::valign': 0,
+ 'th::width': 0,
+ 'thead::align': 0,
+ 'thead::char': 0,
+ 'thead::charoff': 0,
+ 'thead::valign': 0,
+ 'tr::align': 0,
+ 'tr::bgcolor': 0,
+ 'tr::char': 0,
+ 'tr::charoff': 0,
+ 'tr::valign': 0,
+ 'ul::compact': 0,
+ 'ul::type': 0
+};
+html4.eflags = {
+ OPTIONAL_ENDTAG: 1,
+ EMPTY: 2,
+ CDATA: 4,
+ RCDATA: 8,
+ UNSAFE: 16,
+ FOLDABLE: 32,
+ SCRIPT: 64,
+ STYLE: 128
+};
+html4.ELEMENTS = {
+ 'a': 0,
+ 'abbr': 0,
+ 'acronym': 0,
+ 'address': 0,
+ 'applet': 16,
+ 'area': 2,
+ 'b': 0,
+ 'base': 18,
+ 'basefont': 18,
+ 'bdo': 0,
+ 'big': 0,
+ 'blockquote': 0,
+ 'body': 49,
+ 'br': 2,
+ 'button': 0,
+ 'canvas': 0,
+ 'caption': 0,
+ 'center': 0,
+ 'cite': 0,
+ 'code': 0,
+ 'col': 2,
+ 'colgroup': 1,
+ 'dd': 1,
+ 'del': 0,
+ 'dfn': 0,
+ 'dir': 0,
+ 'div': 0,
+ 'dl': 0,
+ 'dt': 1,
+ 'em': 0,
+ 'fieldset': 0,
+ 'font': 0,
+ 'form': 0,
+ 'frame': 18,
+ 'frameset': 16,
+ 'h1': 0,
+ 'h2': 0,
+ 'h3': 0,
+ 'h4': 0,
+ 'h5': 0,
+ 'h6': 0,
+ 'head': 49,
+ 'hr': 2,
+ 'html': 49,
+ 'i': 0,
+ 'iframe': 4,
+ 'img': 2,
+ 'input': 2,
+ 'ins': 0,
+ 'isindex': 18,
+ 'kbd': 0,
+ 'label': 0,
+ 'legend': 0,
+ 'li': 1,
+ 'link': 18,
+ 'map': 0,
+ 'menu': 0,
+ 'meta': 18,
+ 'nobr': 0,
+ 'noembed': 4,
+ 'noframes': 20,
+ 'noscript': 20,
+ 'object': 16,
+ 'ol': 0,
+ 'optgroup': 0,
+ 'option': 1,
+ 'p': 1,
+ 'param': 18,
+ 'pre': 0,
+ 'q': 0,
+ 's': 0,
+ 'samp': 0,
+ 'script': 84,
+ 'select': 0,
+ 'small': 0,
+ 'span': 0,
+ 'strike': 0,
+ 'strong': 0,
+ 'style': 148,
+ 'sub': 0,
+ 'sup': 0,
+ 'table': 0,
+ 'tbody': 1,
+ 'td': 1,
+ 'textarea': 8,
+ 'tfoot': 1,
+ 'th': 1,
+ 'thead': 1,
+ 'title': 24,
+ 'tr': 1,
+ 'tt': 0,
+ 'u': 0,
+ 'ul': 0,
+ 'var': 0
+};
+html4.ueffects = {
+ NOT_LOADED: 0,
+ SAME_DOCUMENT: 1,
+ NEW_DOCUMENT: 2
+};
+html4.URIEFFECTS = {
+ 'a::href': 2,
+ 'area::href': 2,
+ 'blockquote::cite': 0,
+ 'body::background': 1,
+ 'del::cite': 0,
+ 'form::action': 2,
+ 'img::src': 1,
+ 'input::src': 1,
+ 'ins::cite': 0,
+ 'q::cite': 0
+};
+html4.ltypes = {
+ UNSANDBOXED: 2,
+ SANDBOXED: 1,
+ DATA: 0
+};
+html4.LOADERTYPES = {
+ 'a::href': 2,
+ 'area::href': 2,
+ 'blockquote::cite': 2,
+ 'body::background': 1,
+ 'del::cite': 2,
+ 'form::action': 2,
+ 'img::src': 1,
+ 'input::src': 1,
+ 'ins::cite': 2,
+ 'q::cite': 2
+};;
+// Copyright (C) 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview
+ * An HTML sanitizer that can satisfy a variety of security policies.
+ *
+ *
+ * The HTML sanitizer is built around a SAX parser and HTML element and
+ * attributes schemas.
+ *
+ * @author mikesamuel@gmail.com
+ * @requires html4
+ * @overrides window
+ * @provides html, html_sanitize
+ */
+
+/**
+ * @namespace
+ */
+var html = (function (html4) {
+ var lcase;
+ // The below may not be true on browsers in the Turkish locale.
+ if ('script' === 'SCRIPT'.toLowerCase()) {
+ lcase = function (s) { return s.toLowerCase(); };
+ } else {
+ /**
+ * {@updoc
+ * $ lcase('SCRIPT')
+ * # 'script'
+ * $ lcase('script')
+ * # 'script'
+ * }
+ */
+ lcase = function (s) {
+ return s.replace(
+ /[A-Z]/g,
+ function (ch) {
+ return String.fromCharCode(ch.charCodeAt(0) | 32);
+ });
+ };
+ }
+
+ var ENTITIES = {
+ lt : '<',
+ gt : '>',
+ amp : '&',
+ nbsp : '\240',
+ quot : '"',
+ apos : '\''
+ };
+
+ // Schemes on which to defer to uripolicy. Urls with other schemes are denied
+ var WHITELISTED_SCHEMES = /^(?:https?|mailto|data)$/i;
+
+ var decimalEscapeRe = /^#(\d+)$/;
+ var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/;
+ /**
+ * Decodes an HTML entity.
+ *
+ * {@updoc
+ * $ lookupEntity('lt')
+ * # '<'
+ * $ lookupEntity('GT')
+ * # '>'
+ * $ lookupEntity('amp')
+ * # '&'
+ * $ lookupEntity('nbsp')
+ * # '\xA0'
+ * $ lookupEntity('apos')
+ * # "'"
+ * $ lookupEntity('quot')
+ * # '"'
+ * $ lookupEntity('#xa')
+ * # '\n'
+ * $ lookupEntity('#10')
+ * # '\n'
+ * $ lookupEntity('#x0a')
+ * # '\n'
+ * $ lookupEntity('#010')
+ * # '\n'
+ * $ lookupEntity('#x00A')
+ * # '\n'
+ * $ lookupEntity('Pi') // Known failure
+ * # '\u03A0'
+ * $ lookupEntity('pi') // Known failure
+ * # '\u03C0'
+ * }
+ *
+ * @param name the content between the '&' and the ';'.
+ * @return a single unicode code-point as a string.
+ */
+ function lookupEntity(name) {
+ name = lcase(name); // TODO: π is different from Π
+ if (ENTITIES.hasOwnProperty(name)) { return ENTITIES[name]; }
+ var m = name.match(decimalEscapeRe);
+ if (m) {
+ return String.fromCharCode(parseInt(m[1], 10));
+ } else if (!!(m = name.match(hexEscapeRe))) {
+ return String.fromCharCode(parseInt(m[1], 16));
+ }
+ return '';
+ }
+
+ function decodeOneEntity(_, name) {
+ return lookupEntity(name);
+ }
+
+ var nulRe = /\0/g;
+ function stripNULs(s) {
+ return s.replace(nulRe, '');
+ }
+
+ var entityRe = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g;
+ /**
+ * The plain text of a chunk of HTML CDATA which possibly containing.
+ *
+ * {@updoc
+ * $ unescapeEntities('')
+ * # ''
+ * $ unescapeEntities('hello World!')
+ * # 'hello World!'
+ * $ unescapeEntities('1 < 2 && 4 > 3
')
+ * # '1 < 2 && 4 > 3\n'
+ * $ unescapeEntities('<< <- unfinished entity>')
+ * # '<< <- unfinished entity>'
+ * $ unescapeEntities('/foo?bar=baz©=true') // & often unescaped in URLS
+ * # '/foo?bar=baz©=true'
+ * $ unescapeEntities('pi=ππ, Pi=Π\u03A0') // FIXME: known failure
+ * # 'pi=\u03C0\u03c0, Pi=\u03A0\u03A0'
+ * }
+ *
+ * @param s a chunk of HTML CDATA. It must not start or end inside an HTML
+ * entity.
+ */
+ function unescapeEntities(s) {
+ return s.replace(entityRe, decodeOneEntity);
+ }
+
+ var ampRe = /&/g;
+ var looseAmpRe = /&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi;
+ var ltRe = //g;
+ var quotRe = /\"/g;
+ var eqRe = /\=/g; // Backslash required on JScript.net
+
+ /**
+ * Escapes HTML special characters in attribute values as HTML entities.
+ *
+ * {@updoc
+ * $ escapeAttrib('')
+ * # ''
+ * $ escapeAttrib('"<<&==&>>"') // Do not just escape the first occurrence.
+ * # '"<<&==&>>"'
+ * $ escapeAttrib('Hello !')
+ * # 'Hello <World>!'
+ * }
+ */
+ function escapeAttrib(s) {
+ // Escaping '=' defangs many UTF-7 and SGML short-tag attacks.
+ return s.replace(ampRe, '&').replace(ltRe, '<').replace(gtRe, '>')
+ .replace(quotRe, '"').replace(eqRe, '=');
+ }
+
+ /**
+ * Escape entities in RCDATA that can be escaped without changing the meaning.
+ * {@updoc
+ * $ normalizeRCData('1 < 2 && 3 > 4 && 5 < 7&8')
+ * # '1 < 2 && 3 > 4 && 5 < 7&8'
+ * }
+ */
+ function normalizeRCData(rcdata) {
+ return rcdata
+ .replace(looseAmpRe, '&$1')
+ .replace(ltRe, '<')
+ .replace(gtRe, '>');
+ }
+
+
+ // TODO(mikesamuel): validate sanitizer regexs against the HTML5 grammar at
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html
+
+ /** token definitions. */
+ var INSIDE_TAG_TOKEN = new RegExp(
+ // Don't capture space.
+ '^\\s*(?:'
+ // Capture an attribute name in group 1, and value in group 3.
+ // We capture the fact that there was an attribute in group 2, since
+ // interpreters are inconsistent in whether a group that matches nothing
+ // is null, undefined, or the empty string.
+ + ('(?:'
+ + '([a-z][a-z-]*)' // attribute name
+ + ('(' // optionally followed
+ + '\\s*=\\s*'
+ + ('('
+ // A double quoted string.
+ + '\"[^\"]*\"'
+ // A single quoted string.
+ + '|\'[^\']*\''
+ // The positive lookahead is used to make sure that in
+ // , the value for bar is blank, not "baz=boo".
+ + '|(?=[a-z][a-z-]*\\s*=)'
+ // An unquoted value that is not an attribute name.
+ // We know it is not an attribute name because the previous
+ // zero-width match would've eliminated that possibility.
+ + '|[^>\"\'\\s]*'
+ + ')'
+ )
+ + ')'
+ ) + '?'
+ + ')'
+ )
+ // End of tag captured in group 3.
+ + '|(\/?>)'
+ // Don't capture cruft
+ + '|[\\s\\S][^a-z\\s>]*)',
+ 'i');
+
+ var OUTSIDE_TAG_TOKEN = new RegExp(
+ '^(?:'
+ // Entity captured in group 1.
+ + '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);'
+ // Comment, doctypes, and processing instructions not captured.
+ + '|<\!--[\\s\\S]*?--\>|]*>|<\\?[^>*]*>'
+ // '/' captured in group 2 for close tags, and name captured in group 3.
+ + '|<(\/)?([a-z][a-z0-9]*)'
+ // Text captured in group 4.
+ + '|([^<&>]+)'
+ // Cruft captured in group 5.
+ + '|([<&>]))',
+ 'i');
+
+ /**
+ * Given a SAX-like event handler, produce a function that feeds those
+ * events and a parameter to the event handler.
+ *
+ * The event handler has the form:{@code
+ * {
+ * // Name is an upper-case HTML tag name. Attribs is an array of
+ * // alternating upper-case attribute names, and attribute values. The
+ * // attribs array is reused by the parser. Param is the value passed to
+ * // the saxParser.
+ * startTag: function (name, attribs, param) { ... },
+ * endTag: function (name, param) { ... },
+ * pcdata: function (text, param) { ... },
+ * rcdata: function (text, param) { ... },
+ * cdata: function (text, param) { ... },
+ * startDoc: function (param) { ... },
+ * endDoc: function (param) { ... }
+ * }}
+ *
+ * @param {Object} handler a record containing event handlers.
+ * @return {Function} that takes a chunk of html and a parameter.
+ * The parameter is passed on to the handler methods.
+ */
+ function makeSaxParser(handler) {
+ return function parse(htmlText, param) {
+ htmlText = String(htmlText);
+ var htmlLower = null;
+
+ var inTag = false; // True iff we're currently processing a tag.
+ var attribs = []; // Accumulates attribute names and values.
+ var tagName = void 0; // The name of the tag currently being processed.
+ var eflags = void 0; // The element flags for the current tag.
+ var openTag = void 0; // True if the current tag is an open tag.
+
+ if (handler.startDoc) { handler.startDoc(param); }
+
+ while (htmlText) {
+ var m = htmlText.match(inTag ? INSIDE_TAG_TOKEN : OUTSIDE_TAG_TOKEN);
+ htmlText = htmlText.substring(m[0].length);
+
+ if (inTag) {
+ if (m[1]) { // attribute
+ // setAttribute with uppercase names doesn't work on IE6.
+ var attribName = lcase(m[1]);
+ var decodedValue;
+ if (m[2]) {
+ var encodedValue = m[3];
+ switch (encodedValue.charCodeAt(0)) { // Strip quotes
+ case 34: case 39:
+ encodedValue = encodedValue.substring(
+ 1, encodedValue.length - 1);
+ break;
+ }
+ decodedValue = unescapeEntities(stripNULs(encodedValue));
+ } else {
+ // Use name as value for valueless attribs, so
+ //
+ // gets attributes ['type', 'checkbox', 'checked', 'checked']
+ decodedValue = attribName;
+ }
+ attribs.push(attribName, decodedValue);
+ } else if (m[4]) {
+ if (eflags !== void 0) { // False if not in whitelist.
+ if (openTag) {
+ if (handler.startTag) {
+ handler.startTag(tagName, attribs, param);
+ }
+ } else {
+ if (handler.endTag) {
+ handler.endTag(tagName, param);
+ }
+ }
+ }
+
+ if (openTag
+ && (eflags & (html4.eflags.CDATA | html4.eflags.RCDATA))) {
+ if (htmlLower === null) {
+ htmlLower = lcase(htmlText);
+ } else {
+ htmlLower = htmlLower.substring(
+ htmlLower.length - htmlText.length);
+ }
+ var dataEnd = htmlLower.indexOf('' + tagName);
+ if (dataEnd < 0) { dataEnd = htmlText.length; }
+ if (dataEnd) {
+ if (eflags & html4.eflags.CDATA) {
+ if (handler.cdata) {
+ handler.cdata(htmlText.substring(0, dataEnd), param);
+ }
+ } else if (handler.rcdata) {
+ handler.rcdata(
+ normalizeRCData(htmlText.substring(0, dataEnd)), param);
+ }
+ htmlText = htmlText.substring(dataEnd);
+ }
+ }
+
+ tagName = eflags = openTag = void 0;
+ attribs.length = 0;
+ inTag = false;
+ }
+ } else {
+ if (m[1]) { // Entity
+ if (handler.pcdata) { handler.pcdata(m[0], param); }
+ } else if (m[3]) { // Tag
+ openTag = !m[2];
+ inTag = true;
+ tagName = lcase(m[3]);
+ eflags = html4.ELEMENTS.hasOwnProperty(tagName)
+ ? html4.ELEMENTS[tagName] : void 0;
+ } else if (m[4]) { // Text
+ if (handler.pcdata) { handler.pcdata(m[4], param); }
+ } else if (m[5]) { // Cruft
+ if (handler.pcdata) {
+ var ch = m[5];
+ handler.pcdata(
+ ch === '<' ? '<' : ch === '>' ? '>' : '&',
+ param);
+ }
+ }
+ }
+ }
+
+ if (handler.endDoc) { handler.endDoc(param); }
+ };
+ }
+
+ /**
+ * Returns a function that strips unsafe tags and attributes from html.
+ * @param {Function} sanitizeAttributes
+ * maps from (tagName, attribs[]) to null or a sanitized attribute array.
+ * The attribs array can be arbitrarily modified, but the same array
+ * instance is reused, so should not be held.
+ * @return {Function} from html to sanitized html
+ */
+ function makeHtmlSanitizer(sanitizeAttributes) {
+ var stack;
+ var ignoring;
+ return makeSaxParser({
+ startDoc: function (_) {
+ stack = [];
+ ignoring = false;
+ },
+ startTag: function (tagName, attribs, out) {
+ if (ignoring) { return; }
+ if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
+ var eflags = html4.ELEMENTS[tagName];
+ if (eflags & html4.eflags.FOLDABLE) {
+ return;
+ } else if (eflags & html4.eflags.UNSAFE) {
+ ignoring = !(eflags & html4.eflags.EMPTY);
+ return;
+ }
+ attribs = sanitizeAttributes(tagName, attribs);
+ // TODO(mikesamuel): relying on sanitizeAttributes not to
+ // insert unsafe attribute names.
+ if (attribs) {
+ if (!(eflags & html4.eflags.EMPTY)) {
+ stack.push(tagName);
+ }
+
+ out.push('<', tagName);
+ for (var i = 0, n = attribs.length; i < n; i += 2) {
+ var attribName = attribs[i],
+ value = attribs[i + 1];
+ if (value !== null && value !== void 0) {
+ out.push(' ', attribName, '="', escapeAttrib(value), '"');
+ }
+ }
+ out.push('>');
+ }
+ },
+ endTag: function (tagName, out) {
+ if (ignoring) {
+ ignoring = false;
+ return;
+ }
+ if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
+ var eflags = html4.ELEMENTS[tagName];
+ if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY
+ | html4.eflags.FOLDABLE))) {
+ var index;
+ if (eflags & html4.eflags.OPTIONAL_ENDTAG) {
+ for (index = stack.length; --index >= 0;) {
+ var stackEl = stack[index];
+ if (stackEl === tagName) { break; }
+ if (!(html4.ELEMENTS[stackEl]
+ & html4.eflags.OPTIONAL_ENDTAG)) {
+ // Don't pop non optional end tags looking for a match.
+ return;
+ }
+ }
+ } else {
+ for (index = stack.length; --index >= 0;) {
+ if (stack[index] === tagName) { break; }
+ }
+ }
+ if (index < 0) { return; } // Not opened.
+ for (var i = stack.length; --i > index;) {
+ var stackEl = stack[i];
+ if (!(html4.ELEMENTS[stackEl]
+ & html4.eflags.OPTIONAL_ENDTAG)) {
+ out.push('', stackEl, '>');
+ }
+ }
+ stack.length = index;
+ out.push('', tagName, '>');
+ }
+ },
+ pcdata: function (text, out) {
+ if (!ignoring) { out.push(text); }
+ },
+ rcdata: function (text, out) {
+ if (!ignoring) { out.push(text); }
+ },
+ cdata: function (text, out) {
+ if (!ignoring) { out.push(text); }
+ },
+ endDoc: function (out) {
+ for (var i = stack.length; --i >= 0;) {
+ out.push('', stack[i], '>');
+ }
+ stack.length = 0;
+ }
+ });
+ }
+
+ // From RFC3986
+ var URI_SCHEME_RE = new RegExp(
+ "^" +
+ "(?:" +
+ "([^:\/?#]+)" + // scheme
+ ":)?"
+ );
+
+ /**
+ * Strips unsafe tags and attributes from html.
+ * @param {string} htmlText to sanitize
+ * @param {Function} opt_uriPolicy -- a transform to apply to uri/url
+ * attribute values. If no opt_uriPolicy is provided, no uris
+ * are allowed ie. the default uriPolicy rewrites all uris to null
+ * @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to
+ * apply to names, ids, and classes. If no opt_nmTokenPolicy is provided,
+ * all names, ids and classes are passed through ie. the default
+ * nmTokenPolicy is an identity transform
+ * @return {string} html
+ */
+ function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) {
+ var out = [];
+ makeHtmlSanitizer(
+ function sanitizeAttribs(tagName, attribs) {
+ for (var i = 0; i < attribs.length; i += 2) {
+ var attribName = attribs[i];
+ var value = attribs[i + 1];
+ var atype = null, attribKey;
+ if ((attribKey = tagName + '::' + attribName,
+ html4.ATTRIBS.hasOwnProperty(attribKey))
+ || (attribKey = '*::' + attribName,
+ html4.ATTRIBS.hasOwnProperty(attribKey))) {
+ atype = html4.ATTRIBS[attribKey];
+ }
+ if (atype !== null) {
+ switch (atype) {
+ case html4.atype.NONE: break;
+ case html4.atype.SCRIPT:
+ case html4.atype.STYLE:
+ value = null;
+ break;
+ case html4.atype.ID:
+ case html4.atype.IDREF:
+ case html4.atype.IDREFS:
+ case html4.atype.GLOBAL_NAME:
+ case html4.atype.LOCAL_NAME:
+ case html4.atype.CLASSES:
+ value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
+ break;
+ case html4.atype.URI:
+ var parsedUri = ('' + value).match(URI_SCHEME_RE);
+ if (!parsedUri) {
+ value = null;
+ } else if (!parsedUri[1] ||
+ WHITELISTED_SCHEMES.test(parsedUri[1])) {
+ value = opt_uriPolicy && opt_uriPolicy(value);
+ } else {
+ value = null;
+ }
+ break;
+ case html4.atype.URI_FRAGMENT:
+ if (value && '#' === value.charAt(0)) {
+ value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
+ if (value) { value = '#' + value; }
+ } else {
+ value = null;
+ }
+ break;
+ default:
+ value = null;
+ break;
+ }
+ } else {
+ value = null;
+ }
+ attribs[i + 1] = value;
+ }
+ return attribs;
+ })(htmlText, out);
+ return out.join('');
+ }
+
+ return {
+ escapeAttrib: escapeAttrib,
+ makeHtmlSanitizer: makeHtmlSanitizer,
+ makeSaxParser: makeSaxParser,
+ normalizeRCData: normalizeRCData,
+ sanitize: sanitize,
+ unescapeEntities: unescapeEntities
+ };
+})(html4);
+
+var html_sanitize = html.sanitize;
+
+// Exports for closure compiler. Note this file is also cajoled
+// for domado and run in an environment without 'window'
+if (typeof window !== 'undefined') {
+ window['html'] = html;
+ window['html_sanitize'] = html_sanitize;
+}
+// Loosen restrictions of Caja's
+// html-sanitizer to allow for styling
+html4.ATTRIBS['*::style'] = 0;
+html4.ELEMENTS['style'] = 0;
+
+html4.ATTRIBS['a::target'] = 0;
+
+html4.ELEMENTS['video'] = 0;
+html4.ATTRIBS['video::src'] = 0;
+html4.ATTRIBS['video::poster'] = 0;
+html4.ATTRIBS['video::controls'] = 0;
+
+html4.ELEMENTS['audio'] = 0;
+html4.ATTRIBS['audio::src'] = 0;
+html4.ATTRIBS['video::autoplay'] = 0;
+html4.ATTRIBS['video::controls'] = 0;
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+var Mustache = (typeof module !== "undefined" && module.exports) || {};
+
+(function (exports) {
+
+ exports.name = "mustache.js";
+ exports.version = "0.5.0-dev";
+ exports.tags = ["{{", "}}"];
+ exports.parse = parse;
+ exports.compile = compile;
+ exports.render = render;
+ exports.clearCache = clearCache;
+
+ // This is here for backwards compatibility with 0.4.x.
+ exports.to_html = function (template, view, partials, send) {
+ var result = render(template, view, partials);
+
+ if (typeof send === "function") {
+ send(result);
+ } else {
+ return result;
+ }
+ };
+
+ var _toString = Object.prototype.toString;
+ var _isArray = Array.isArray;
+ var _forEach = Array.prototype.forEach;
+ var _trim = String.prototype.trim;
+
+ var isArray;
+ if (_isArray) {
+ isArray = _isArray;
+ } else {
+ isArray = function (obj) {
+ return _toString.call(obj) === "[object Array]";
+ };
+ }
+
+ var forEach;
+ if (_forEach) {
+ forEach = function (obj, callback, scope) {
+ return _forEach.call(obj, callback, scope);
+ };
+ } else {
+ forEach = function (obj, callback, scope) {
+ for (var i = 0, len = obj.length; i < len; ++i) {
+ callback.call(scope, obj[i], i, obj);
+ }
+ };
+ }
+
+ var spaceRe = /^\s*$/;
+
+ function isWhitespace(string) {
+ return spaceRe.test(string);
+ }
+
+ var trim;
+ if (_trim) {
+ trim = function (string) {
+ return string == null ? "" : _trim.call(string);
+ };
+ } else {
+ var trimLeft, trimRight;
+
+ if (isWhitespace("\xA0")) {
+ trimLeft = /^\s+/;
+ trimRight = /\s+$/;
+ } else {
+ // IE doesn't match non-breaking spaces with \s, thanks jQuery.
+ trimLeft = /^[\s\xA0]+/;
+ trimRight = /[\s\xA0]+$/;
+ }
+
+ trim = function (string) {
+ return string == null ? "" :
+ String(string).replace(trimLeft, "").replace(trimRight, "");
+ };
+ }
+
+ var escapeMap = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': '"',
+ "'": ''',
+ "/": '/'
+ };
+
+ function escapeHTML(string) {
+ return String(string).replace(/[&<>"'\/]/g, function (s) {
+ return escapeMap[s] || s;
+ });
+ }
+
+ /**
+ * Adds the `template`, `line`, and `file` properties to the given error
+ * object and alters the message to provide more useful debugging information.
+ */
+ function debug(e, template, line, file) {
+ file = file || "";
+
+ var lines = template.split("\n"),
+ start = Math.max(line - 3, 0),
+ end = Math.min(lines.length, line + 3),
+ context = lines.slice(start, end);
+
+ var c;
+ for (var i = 0, len = context.length; i < len; ++i) {
+ c = i + start + 1;
+ context[i] = (c === line ? " >> " : " ") + context[i];
+ }
+
+ e.template = template;
+ e.line = line;
+ e.file = file;
+ e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n");
+
+ return e;
+ }
+
+ /**
+ * Looks up the value of the given `name` in the given context `stack`.
+ */
+ function lookup(name, stack, defaultValue) {
+ if (name === ".") {
+ return stack[stack.length - 1];
+ }
+
+ var names = name.split(".");
+ var lastIndex = names.length - 1;
+ var target = names[lastIndex];
+
+ var value, context, i = stack.length, j, localStack;
+ while (i) {
+ localStack = stack.slice(0);
+ context = stack[--i];
+
+ j = 0;
+ while (j < lastIndex) {
+ context = context[names[j++]];
+
+ if (context == null) {
+ break;
+ }
+
+ localStack.push(context);
+ }
+
+ if (context && typeof context === "object" && target in context) {
+ value = context[target];
+ break;
+ }
+ }
+
+ // If the value is a function, call it in the current context.
+ if (typeof value === "function") {
+ value = value.call(localStack[localStack.length - 1]);
+ }
+
+ if (value == null) {
+ return defaultValue;
+ }
+
+ return value;
+ }
+
+ function renderSection(name, stack, callback, inverted) {
+ var buffer = "";
+ var value = lookup(name, stack);
+
+ if (inverted) {
+ // From the spec: inverted sections may render text once based on the
+ // inverse value of the key. That is, they will be rendered if the key
+ // doesn't exist, is false, or is an empty list.
+ if (value == null || value === false || (isArray(value) && value.length === 0)) {
+ buffer += callback();
+ }
+ } else if (isArray(value)) {
+ forEach(value, function (value) {
+ stack.push(value);
+ buffer += callback();
+ stack.pop();
+ });
+ } else if (typeof value === "object") {
+ stack.push(value);
+ buffer += callback();
+ stack.pop();
+ } else if (typeof value === "function") {
+ var scope = stack[stack.length - 1];
+ var scopedRender = function (template) {
+ return render(template, scope);
+ };
+ buffer += value.call(scope, callback(), scopedRender) || "";
+ } else if (value) {
+ buffer += callback();
+ }
+
+ return buffer;
+ }
+
+ /**
+ * Parses the given `template` and returns the source of a function that,
+ * with the proper arguments, will render the template. Recognized options
+ * include the following:
+ *
+ * - file The name of the file the template comes from (displayed in
+ * error messages)
+ * - tags An array of open and close tags the `template` uses. Defaults
+ * to the value of Mustache.tags
+ * - debug Set `true` to log the body of the generated function to the
+ * console
+ * - space Set `true` to preserve whitespace from lines that otherwise
+ * contain only a {{tag}}. Defaults to `false`
+ */
+ function parse(template, options) {
+ options = options || {};
+
+ var tags = options.tags || exports.tags,
+ openTag = tags[0],
+ closeTag = tags[tags.length - 1];
+
+ var code = [
+ 'var buffer = "";', // output buffer
+ "\nvar line = 1;", // keep track of source line number
+ "\ntry {",
+ '\nbuffer += "'
+ ];
+
+ var spaces = [], // indices of whitespace in code on the current line
+ hasTag = false, // is there a {{tag}} on the current line?
+ nonSpace = false; // is there a non-space char on the current line?
+
+ // Strips all space characters from the code array for the current line
+ // if there was a {{tag}} on it and otherwise only spaces.
+ var stripSpace = function () {
+ if (hasTag && !nonSpace && !options.space) {
+ while (spaces.length) {
+ code.splice(spaces.pop(), 1);
+ }
+ } else {
+ spaces = [];
+ }
+
+ hasTag = false;
+ nonSpace = false;
+ };
+
+ var sectionStack = [], updateLine, nextOpenTag, nextCloseTag;
+
+ var setTags = function (source) {
+ tags = trim(source).split(/\s+/);
+ nextOpenTag = tags[0];
+ nextCloseTag = tags[tags.length - 1];
+ };
+
+ var includePartial = function (source) {
+ code.push(
+ '";',
+ updateLine,
+ '\nvar partial = partials["' + trim(source) + '"];',
+ '\nif (partial) {',
+ '\n buffer += render(partial,stack[stack.length - 1],partials);',
+ '\n}',
+ '\nbuffer += "'
+ );
+ };
+
+ var openSection = function (source, inverted) {
+ var name = trim(source);
+
+ if (name === "") {
+ throw debug(new Error("Section name may not be empty"), template, line, options.file);
+ }
+
+ sectionStack.push({name: name, inverted: inverted});
+
+ code.push(
+ '";',
+ updateLine,
+ '\nvar name = "' + name + '";',
+ '\nvar callback = (function () {',
+ '\n return function () {',
+ '\n var buffer = "";',
+ '\nbuffer += "'
+ );
+ };
+
+ var openInvertedSection = function (source) {
+ openSection(source, true);
+ };
+
+ var closeSection = function (source) {
+ var name = trim(source);
+ var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name;
+
+ if (!openName || name != openName) {
+ throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file);
+ }
+
+ var section = sectionStack.pop();
+
+ code.push(
+ '";',
+ '\n return buffer;',
+ '\n };',
+ '\n})();'
+ );
+
+ if (section.inverted) {
+ code.push("\nbuffer += renderSection(name,stack,callback,true);");
+ } else {
+ code.push("\nbuffer += renderSection(name,stack,callback);");
+ }
+
+ code.push('\nbuffer += "');
+ };
+
+ var sendPlain = function (source) {
+ code.push(
+ '";',
+ updateLine,
+ '\nbuffer += lookup("' + trim(source) + '",stack,"");',
+ '\nbuffer += "'
+ );
+ };
+
+ var sendEscaped = function (source) {
+ code.push(
+ '";',
+ updateLine,
+ '\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));',
+ '\nbuffer += "'
+ );
+ };
+
+ var line = 1, c, callback;
+ for (var i = 0, len = template.length; i < len; ++i) {
+ if (template.slice(i, i + openTag.length) === openTag) {
+ i += openTag.length;
+ c = template.substr(i, 1);
+ updateLine = '\nline = ' + line + ';';
+ nextOpenTag = openTag;
+ nextCloseTag = closeTag;
+ hasTag = true;
+
+ switch (c) {
+ case "!": // comment
+ i++;
+ callback = null;
+ break;
+ case "=": // change open/close tags, e.g. {{=<% %>=}}
+ i++;
+ closeTag = "=" + closeTag;
+ callback = setTags;
+ break;
+ case ">": // include partial
+ i++;
+ callback = includePartial;
+ break;
+ case "#": // start section
+ i++;
+ callback = openSection;
+ break;
+ case "^": // start inverted section
+ i++;
+ callback = openInvertedSection;
+ break;
+ case "/": // end section
+ i++;
+ callback = closeSection;
+ break;
+ case "{": // plain variable
+ closeTag = "}" + closeTag;
+ // fall through
+ case "&": // plain variable
+ i++;
+ nonSpace = true;
+ callback = sendPlain;
+ break;
+ default: // escaped variable
+ nonSpace = true;
+ callback = sendEscaped;
+ }
+
+ var end = template.indexOf(closeTag, i);
+
+ if (end === -1) {
+ throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file);
+ }
+
+ var source = template.substring(i, end);
+
+ if (callback) {
+ callback(source);
+ }
+
+ // Maintain line count for \n in source.
+ var n = 0;
+ while (~(n = source.indexOf("\n", n))) {
+ line++;
+ n++;
+ }
+
+ i = end + closeTag.length - 1;
+ openTag = nextOpenTag;
+ closeTag = nextCloseTag;
+ } else {
+ c = template.substr(i, 1);
+
+ switch (c) {
+ case '"':
+ case "\\":
+ nonSpace = true;
+ code.push("\\" + c);
+ break;
+ case "\r":
+ // Ignore carriage returns.
+ break;
+ case "\n":
+ spaces.push(code.length);
+ code.push("\\n");
+ stripSpace(); // Check for whitespace on the current line.
+ line++;
+ break;
+ default:
+ if (isWhitespace(c)) {
+ spaces.push(code.length);
+ } else {
+ nonSpace = true;
+ }
+
+ code.push(c);
+ }
+ }
+ }
+
+ if (sectionStack.length != 0) {
+ throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file);
+ }
+
+ // Clean up any whitespace from a closing {{tag}} that was at the end
+ // of the template without a trailing \n.
+ stripSpace();
+
+ code.push(
+ '";',
+ "\nreturn buffer;",
+ "\n} catch (e) { throw {error: e, line: line}; }"
+ );
+
+ // Ignore `buffer += "";` statements.
+ var body = code.join("").replace(/buffer \+= "";\n/g, "");
+
+ if (options.debug) {
+ if (typeof console != "undefined" && console.log) {
+ console.log(body);
+ } else if (typeof print === "function") {
+ print(body);
+ }
+ }
+
+ return body;
+ }
+
+ /**
+ * Used by `compile` to generate a reusable function for the given `template`.
+ */
+ function _compile(template, options) {
+ var args = "view,partials,stack,lookup,escapeHTML,renderSection,render";
+ var body = parse(template, options);
+ var fn = new Function(args, body);
+
+ // This anonymous function wraps the generated function so we can do
+ // argument coercion, setup some variables, and handle any errors
+ // encountered while executing it.
+ return function (view, partials) {
+ partials = partials || {};
+
+ var stack = [view]; // context stack
+
+ try {
+ return fn(view, partials, stack, lookup, escapeHTML, renderSection, render);
+ } catch (e) {
+ throw debug(e.error, template, e.line, options.file);
+ }
+ };
+ }
+
+ // Cache of pre-compiled templates.
+ var _cache = {};
+
+ /**
+ * Clear the cache of compiled templates.
+ */
+ function clearCache() {
+ _cache = {};
+ }
+
+ /**
+ * Compiles the given `template` into a reusable function using the given
+ * `options`. In addition to the options accepted by Mustache.parse,
+ * recognized options include the following:
+ *
+ * - cache Set `false` to bypass any pre-compiled version of the given
+ * template. Otherwise, a given `template` string will be cached
+ * the first time it is parsed
+ */
+ function compile(template, options) {
+ options = options || {};
+
+ // Use a pre-compiled version from the cache if we have one.
+ if (options.cache !== false) {
+ if (!_cache[template]) {
+ _cache[template] = _compile(template, options);
+ }
+
+ return _cache[template];
+ }
+
+ return _compile(template, options);
+ }
+
+ /**
+ * High-level function that renders the given `template` using the given
+ * `view` and `partials`. If you need to use any of the template options (see
+ * `compile` above), you must compile in a separate step, and then call that
+ * compiled function.
+ */
+ function render(template, view, partials) {
+ return compile(template)(view, partials);
+ }
+
+})(Mustache);
+
+wax = wax || {};
+
+// Attribution
+// -----------
+wax.attribution = function() {
+ var container,
+ a = {};
+
+ function urlX(url) {
+ // Data URIs are subject to a bug in Firefox
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=255107
+ // which let them be a vector. But WebKit does 'the right thing'
+ // or at least 'something' about this situation, so we'll tolerate
+ // them.
+ if (/^(https?:\/\/|data:image)/.test(url)) {
+ return url;
+ }
+ }
+
+ function idX(id) {
+ return id;
+ }
+
+ a.content = function(x) {
+ if (typeof x === 'undefined') return container.innerHTML;
+ container.innerHTML = html_sanitize(x, urlX, idX);
+ return this;
+ };
+
+ a.element = function() {
+ return container;
+ };
+
+ a.init = function() {
+ container = document.createElement('div');
+ container.className = 'wax-attribution';
+ return this;
+ };
+
+ return a.init();
+};
+wax = wax || {};
+
+// Attribution
+// -----------
+wax.bwdetect = function(options, callback) {
+ var detector = {},
+ threshold = options.threshold || 400,
+ // test image: 30.29KB
+ testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()),
+ // High-bandwidth assumed
+ // 1: high bandwidth (.png, .jpg)
+ // 0: low bandwidth (.png128, .jpg70)
+ bw = 1,
+ // Alternative versions
+ auto = options.auto === undefined ? true : options.auto;
+
+ function bwTest() {
+ wax.bw = -1;
+ var im = new Image();
+ im.src = testImage;
+ var first = true;
+ var timeout = setTimeout(function() {
+ if (first && wax.bw == -1) {
+ detector.bw(0);
+ first = false;
+ }
+ }, threshold);
+ im.onload = function() {
+ if (first && wax.bw == -1) {
+ clearTimeout(timeout);
+ detector.bw(1);
+ first = false;
+ }
+ };
+ }
+
+ detector.bw = function(x) {
+ if (!arguments.length) return bw;
+ var oldBw = bw;
+ if (wax.bwlisteners && wax.bwlisteners.length) (function () {
+ listeners = wax.bwlisteners;
+ wax.bwlisteners = [];
+ for (i = 0; i < listeners; i++) {
+ listeners[i](x);
+ }
+ })();
+ wax.bw = x;
+
+ if (bw != (bw = x)) callback(x);
+ };
+
+ detector.add = function() {
+ if (auto) bwTest();
+ return this;
+ };
+
+ if (wax.bw == -1) {
+ wax.bwlisteners = wax.bwlisteners || [];
+ wax.bwlisteners.push(detector.bw);
+ } else if (wax.bw !== undefined) {
+ detector.bw(wax.bw);
+ } else {
+ detector.add();
+ }
+ return detector;
+};
+// Formatter
+// ---------
+//
+// This code is no longer the recommended code path for Wax -
+// see `template.js`, a safe implementation of Mustache templates.
+wax.formatter = function(x) {
+ var formatter = {},
+ f;
+
+ // Prevent against just any input being used.
+ if (x && typeof x === 'string') {
+ try {
+ // Ugly, dangerous use of eval.
+ eval('f = ' + x);
+ } catch (e) {
+ if (console) console.log(e);
+ }
+ } else if (x && typeof x === 'function') {
+ f = x;
+ } else {
+ f = function() {};
+ }
+
+ function urlX(url) {
+ if (/^(https?:\/\/|data:image)/.test(url)) {
+ return url;
+ }
+ }
+
+ function idX(id) {
+ return id;
+ }
+
+ // Wrap the given formatter function in order to
+ // catch exceptions that it may throw.
+ formatter.format = function(options, data) {
+ try {
+ return html_sanitize(f(options, data), urlX, idX);
+ } catch (e) {
+ if (console) console.log(e);
+ }
+ };
+
+ return formatter;
+};
+// GridInstance
+// ------------
+// GridInstances are queryable, fully-formed
+// objects for acquiring features from events.
+//
+// This code ignores format of 1.1-1.2
+wax.gi = function(grid_tile, options) {
+ options = options || {};
+ // resolution is the grid-elements-per-pixel ratio of gridded data.
+ // The size of a tile element. For now we expect tiles to be squares.
+ var instance = {},
+ resolution = options.resolution || 4,
+ tileSize = options.tileSize || 256;
+
+ // Resolve the UTF-8 encoding stored in grids to simple
+ // number values.
+ // See the [utfgrid spec](https://github.com/mapbox/utfgrid-spec)
+ // for details.
+ function resolveCode(key) {
+ if (key >= 93) key--;
+ if (key >= 35) key--;
+ key -= 32;
+ return key;
+ }
+
+ instance.grid_tile = function() {
+ return grid_tile;
+ };
+
+ instance.getKey = function(x, y) {
+ if (!(grid_tile && grid_tile.grid)) return;
+ if ((y < 0) || (x < 0)) return;
+ if ((Math.floor(y) >= tileSize) ||
+ (Math.floor(x) >= tileSize)) return;
+ // Find the key in the grid. The above calls should ensure that
+ // the grid's array is large enough to make this work.
+ return resolveCode(grid_tile.grid[
+ Math.floor((y) / resolution)
+ ].charCodeAt(
+ Math.floor((x) / resolution)
+ ));
+ };
+
+ // Lower-level than tileFeature - has nothing to do
+ // with the DOM. Takes a px offset from 0, 0 of a grid.
+ instance.gridFeature = function(x, y) {
+ // Find the key in the grid. The above calls should ensure that
+ // the grid's array is large enough to make this work.
+ var key = this.getKey(x, y),
+ keys = grid_tile.keys;
+
+ if (keys &&
+ keys[key] &&
+ grid_tile.data[keys[key]]) {
+ return grid_tile.data[keys[key]];
+ }
+ };
+
+ // Get a feature:
+ // * `x` and `y`: the screen coordinates of an event
+ // * `tile_element`: a DOM element of a tile, from which we can get an offset.
+ instance.tileFeature = function(x, y, tile_element) {
+ if (!grid_tile) return;
+ // IE problem here - though recoverable, for whatever reason
+ var offset = wax.u.offset(tile_element);
+ feature = this.gridFeature(x - offset.left, y - offset.top);
+ return feature;
+ };
+
+ return instance;
+};
+// GridManager
+// -----------
+// Generally one GridManager will be used per map.
+//
+// It takes one options object, which current accepts a single option:
+// `resolution` determines the number of pixels per grid element in the grid.
+// The default is 4.
+wax.gm = function() {
+
+ var resolution = 4,
+ grid_tiles = {},
+ manager = {},
+ tilejson,
+ formatter;
+
+ var gridUrl = function(url) {
+ return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json');
+ };
+
+ function templatedGridUrl(template) {
+ if (typeof template === 'string') template = [template];
+ return function templatedGridFinder(url) {
+ if (!url) return;
+ var rx = new RegExp('/(\\d+)\\/(\\d+)\\/(\\d+)\\.[\\w\\._]+');
+ var xyz = rx.exec(url);
+ if (!xyz) return;
+ return template[parseInt(xyz[2], 10) % template.length]
+ .replace(/\{z\}/g, xyz[1])
+ .replace(/\{x\}/g, xyz[2])
+ .replace(/\{y\}/g, xyz[3]);
+ };
+ }
+
+ manager.formatter = function(x) {
+ if (!arguments.length) return formatter;
+ formatter = wax.formatter(x);
+ return manager;
+ };
+
+ manager.template = function(x) {
+ if (!arguments.length) return formatter;
+ formatter = wax.template(x);
+ return manager;
+ };
+
+ manager.gridUrl = function(x) {
+ if (!arguments.length) return gridUrl;
+ gridUrl = typeof x === 'function' ?
+ x : templatedGridUrl(x);
+ return manager;
+ };
+
+ manager.getGrid = function(url, callback) {
+ var gurl = gridUrl(url);
+ if (!formatter || !gurl) return callback(null, null);
+
+ wax.request.get(gurl, function(err, t) {
+ if (err) return callback(err, null);
+ callback(null, wax.gi(t, {
+ formatter: formatter,
+ resolution: resolution
+ }));
+ });
+ return manager;
+ };
+
+ manager.tilejson = function(x) {
+ if (!arguments.length) return tilejson;
+ // prefer templates over formatters
+ if (x.template) {
+ manager.template(x.template);
+ } else if (x.formatter) {
+ manager.formatter(x.formatter);
+ } else {
+ formatter = undefined;
+ }
+ if (x.grids) manager.gridUrl(x.grids);
+ if (x.resolution) resolution = x.resolution;
+ tilejson = x;
+ return manager;
+ };
+
+ return manager;
+};
+wax = wax || {};
+
+// Hash
+// ----
+wax.hash = function(options) {
+ options = options || {};
+
+ function getState() {
+ return location.hash.substring(1);
+ }
+
+ function pushState(state) {
+ var l = window.location;
+ l.replace(l.toString().replace((l.hash || /$/), '#' + state));
+ }
+
+ var s0, // old hash
+ hash = {},
+ lat = 90 - 1e-8; // allowable latitude range
+
+ function parseHash(s) {
+ var args = s.split('/');
+ for (var i = 0; i < args.length; i++) {
+ args[i] = Number(args[i]);
+ if (isNaN(args[i])) return true;
+ }
+ if (args.length < 3) {
+ // replace bogus hash
+ return true;
+ } else if (args.length == 3) {
+ options.setCenterZoom(args);
+ }
+ }
+
+ function move() {
+ var s1 = options.getCenterZoom();
+ if (s0 !== s1) {
+ s0 = s1;
+ // don't recenter the map!
+ pushState(s0);
+ }
+ }
+
+ function stateChange(state) {
+ // ignore spurious hashchange events
+ if (state === s0) return;
+ if (parseHash(s0 = state)) {
+ // replace bogus hash
+ move();
+ }
+ }
+
+ var _move = wax.u.throttle(move, 500);
+
+ hash.add = function() {
+ stateChange(getState());
+ options.bindChange(_move);
+ return this;
+ };
+
+ hash.remove = function() {
+ options.unbindChange(_move);
+ return this;
+ };
+
+ return hash.add();
+};
+wax = wax || {};
+
+wax.interaction = function() {
+ var gm = wax.gm(),
+ interaction = {},
+ _downLock = false,
+ _clickTimeout = false,
+ // Active feature
+ // Down event
+ _d,
+ // Touch tolerance
+ tol = 4,
+ grid,
+ attach,
+ detach,
+ parent,
+ map,
+ tileGrid;
+
+ var defaultEvents = {
+ mousemove: onMove,
+ touchstart: onDown,
+ mousedown: onDown
+ };
+
+ var touchEnds = {
+ touchend: onUp,
+ touchmove: onUp,
+ touchcancel: touchCancel
+ };
+
+ // Abstract getTile method. Depends on a tilegrid with
+ // grid[ [x, y, tile] ] structure.
+ function getTile(e) {
+ var g = grid();
+ for (var i = 0; i < g.length; i++) {
+ if ((g[i][0] < e.y) &&
+ ((g[i][0] + 256) > e.y) &&
+ (g[i][1] < e.x) &&
+ ((g[i][1] + 256) > e.x)) return g[i][2];
+ }
+ return false;
+ }
+
+ // Clear the double-click timeout to prevent double-clicks from
+ // triggering popups.
+ function killTimeout() {
+ if (_clickTimeout) {
+ window.clearTimeout(_clickTimeout);
+ _clickTimeout = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ function onMove(e) {
+ // If the user is actually dragging the map, exit early
+ // to avoid performance hits.
+ if (_downLock) return;
+
+ var pos = wax.u.eventoffset(e);
+
+ interaction.screen_feature(pos, function(feature) {
+ if (feature) {
+ bean.fire(interaction, 'on', {
+ parent: parent(),
+ data: feature,
+ formatter: gm.formatter().format,
+ e: e
+ });
+ } else {
+ bean.fire(interaction, 'off');
+ }
+ });
+ }
+
+ // A handler for 'down' events - which means `mousedown` and `touchstart`
+ function onDown(e) {
+ // Ignore double-clicks by ignoring clicks within 300ms of
+ // each other.
+ if (killTimeout()) { return; }
+
+ // Prevent interaction offset calculations happening while
+ // the user is dragging the map.
+ //
+ // Store this event so that we can compare it to the
+ // up event
+ _downLock = true;
+ _d = wax.u.eventoffset(e);
+ if (e.type === 'mousedown') {
+ bean.add(document.body, 'click', onUp);
+
+ // Only track single-touches. Double-touches will not affect this
+ // control
+ } else if (e.type === 'touchstart' && e.touches.length === 1) {
+ // Don't make the user click close if they hit another tooltip
+ bean.fire(interaction, 'off');
+ // Touch moves invalidate touches
+ bean.add(parent(), touchEnds);
+ }
+ }
+
+ function touchCancel() {
+ bean.remove(parent(), touchEnds);
+ _downLock = false;
+ }
+
+ function onUp(e) {
+ var evt = {},
+ pos = wax.u.eventoffset(e);
+ _downLock = false;
+
+ // TODO: refine
+ for (var key in e) {
+ evt[key] = e[key];
+ }
+
+ bean.remove(document.body, 'mouseup', onUp);
+ bean.remove(parent(), touchEnds);
+
+ if (e.type === 'touchend') {
+ // If this was a touch and it survived, there's no need to avoid a double-tap
+ // but also wax.u.eventoffset will have failed, since this touch
+ // event doesn't have coordinates
+ interaction.click(e, _d);
+ } else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) &&
+ Math.round(pos.x / tol) === Math.round(_d.x / tol)) {
+ // Contain the event data in a closure.
+ _clickTimeout = window.setTimeout(
+ function() {
+ _clickTimeout = null;
+ interaction.click(evt, pos);
+ }, 300);
+ }
+ return onUp;
+ }
+
+ // Handle a click event. Takes a second
+ interaction.click = function(e, pos) {
+ interaction.screen_feature(pos, function(feature) {
+ if (feature) bean.fire(interaction, 'on', {
+ parent: parent(),
+ data: feature,
+ formatter: gm.formatter().format,
+ e: e
+ });
+ });
+ };
+
+ interaction.screen_feature = function(pos, callback) {
+ var tile = getTile(pos);
+ if (!tile) callback(null);
+ gm.getGrid(tile.src, function(err, g) {
+ if (err || !g) return callback(null);
+ var feature = g.tileFeature(pos.x, pos.y, tile);
+ callback(feature);
+ });
+ };
+
+ // set an attach function that should be
+ // called when maps are set
+ interaction.attach = function(x) {
+ if (!arguments.length) return attach;
+ attach = x;
+ return interaction;
+ };
+
+ interaction.detach = function(x) {
+ if (!arguments.length) return detach;
+ detach = x;
+ return interaction;
+ };
+
+ // Attach listeners to the map
+ interaction.map = function(x) {
+ if (!arguments.length) return map;
+ map = x;
+ if (attach) attach(map);
+ bean.add(parent(), defaultEvents);
+ bean.add(parent(), 'touchstart', onDown);
+ return interaction;
+ };
+
+ // set a grid getter for this control
+ interaction.grid = function(x) {
+ if (!arguments.length) return grid;
+ grid = x;
+ return interaction;
+ };
+
+ // detach this and its events from the map cleanly
+ interaction.remove = function(x) {
+ if (detach) detach(map);
+ bean.remove(parent(), defaultEvents);
+ bean.fire(interaction, 'remove');
+ return interaction;
+ };
+
+ // get or set a tilejson chunk of json
+ interaction.tilejson = function(x) {
+ if (!arguments.length) return gm.tilejson();
+ gm.tilejson(x);
+ return interaction;
+ };
+
+ // return the formatter, which has an exposed .format
+ // function
+ interaction.formatter = function() {
+ return gm.formatter();
+ };
+
+ // ev can be 'on', 'off', fn is the handler
+ interaction.on = function(ev, fn) {
+ bean.add(interaction, ev, fn);
+ return interaction;
+ };
+
+ // ev can be 'on', 'off', fn is the handler
+ interaction.off = function(ev, fn) {
+ bean.remove(interaction, ev, fn);
+ return interaction;
+ };
+
+ // Return or set the gridmanager implementation
+ interaction.gridmanager = function(x) {
+ if (!arguments.length) return gm;
+ gm = x;
+ return interaction;
+ };
+
+ // parent should be a function that returns
+ // the parent element of the map
+ interaction.parent = function(x) {
+ parent = x;
+ return interaction;
+ };
+
+ return interaction;
+};
+// Wax Legend
+// ----------
+
+// Wax header
+var wax = wax || {};
+
+wax.legend = function() {
+ var element,
+ legend = {},
+ container;
+
+ function urlX(url) {
+ // Data URIs are subject to a bug in Firefox
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=255107
+ // which let them be a vector. But WebKit does 'the right thing'
+ // or at least 'something' about this situation, so we'll tolerate
+ // them.
+ if (/^(https?:\/\/|data:image)/.test(url)) {
+ return url;
+ }
+ }
+
+ function idX(id) {
+ return id;
+ }
+
+ legend.element = function() {
+ return container;
+ };
+
+ legend.content = function(content) {
+ if (!arguments.length) return element.innerHTML;
+ if (content) {
+ element.innerHTML = html_sanitize(content, urlX, idX);
+ element.style.display = 'block';
+ } else {
+ element.innerHTML = '';
+ element.style.display = 'none';
+ }
+ return legend;
+ };
+
+ legend.add = function() {
+ container = document.createElement('div');
+ container.className = 'wax-legends';
+
+ element = container.appendChild(document.createElement('div'));
+ element.className = 'wax-legend';
+ element.style.display = 'none';
+ return legend;
+ };
+
+ return legend.add();
+};
+var wax = wax || {};
+
+wax.location = function() {
+
+ var t = {};
+
+ function on(o) {
+ console.log(o);
+ if ((o.e.type === 'mousemove' || !o.e.type)) {
+ return;
+ } else {
+ var loc = o.formatter({ format: 'location' }, o.data);
+ if (loc) {
+ window.location.href = loc;
+ }
+ }
+ }
+
+ t.events = function() {
+ return {
+ on: on
+ };
+ };
+
+ return t;
+
+};
+var wax = wax || {};
+wax.movetip = {};
+
+wax.movetip = function() {
+ var popped = false,
+ t = {},
+ _tooltipOffset,
+ _contextOffset,
+ tooltip,
+ parent;
+
+ function moveTooltip(e) {
+ var eo = wax.u.eventoffset(e);
+ // faux-positioning
+ if ((_tooltipOffset.height + eo.y) >
+ (_contextOffset.top + _contextOffset.height) &&
+ (_contextOffset.height > _tooltipOffset.height)) {
+ eo.y -= _tooltipOffset.height;
+ tooltip.className += ' flip-y';
+ }
+
+ // faux-positioning
+ if ((_tooltipOffset.width + eo.x) >
+ (_contextOffset.left + _contextOffset.width)) {
+ eo.x -= _tooltipOffset.width;
+ tooltip.className += ' flip-x';
+ }
+
+ tooltip.style.left = eo.x + 'px';
+ tooltip.style.top = eo.y + 'px';
+ }
+
+ // Get the active tooltip for a layer or create a new one if no tooltip exists.
+ // Hide any tooltips on layers underneath this one.
+ function getTooltip(feature) {
+ var tooltip = document.createElement('div');
+ tooltip.className = 'wax-tooltip wax-tooltip-0';
+ tooltip.innerHTML = feature;
+ return tooltip;
+ }
+
+ // Hide a given tooltip.
+ function hide() {
+ if (tooltip) {
+ tooltip.parentNode.removeChild(tooltip);
+ tooltip = null;
+ }
+ }
+
+ function on(o) {
+ var content;
+ if (popped) return;
+ if ((o.e.type === 'mousemove' || !o.e.type)) {
+ content = o.formatter({ format: 'teaser' }, o.data);
+ if (!content) return;
+ hide();
+ parent.style.cursor = 'pointer';
+ tooltip = document.body.appendChild(getTooltip(content));
+ } else {
+ content = o.formatter({ format: 'teaser' }, o.data);
+ if (!content) return;
+ hide();
+ var tt = document.body.appendChild(getTooltip(content));
+ tt.className += ' wax-popup';
+
+ var close = tt.appendChild(document.createElement('a'));
+ close.href = '#close';
+ close.className = 'close';
+ close.innerHTML = 'Close';
+
+ popped = true;
+
+ tooltip = tt;
+
+ _tooltipOffset = wax.u.offset(tooltip);
+ _contextOffset = wax.u.offset(parent);
+ moveTooltip(o.e);
+
+ bean.add(close, 'click touchend', function closeClick(e) {
+ e.stop();
+ hide();
+ popped = false;
+ });
+ }
+ if (tooltip) {
+ _tooltipOffset = wax.u.offset(tooltip);
+ _contextOffset = wax.u.offset(parent);
+ moveTooltip(o.e);
+ }
+
+ }
+
+ function off() {
+ parent.style.cursor = 'default';
+ if (!popped) hide();
+ }
+
+ t.parent = function(x) {
+ if (!arguments.length) return parent;
+ parent = x;
+ return t;
+ };
+
+ t.events = function() {
+ return {
+ on: on,
+ off: off
+ };
+ };
+
+ return t;
+};
+
+// Wax GridUtil
+// ------------
+
+// Wax header
+var wax = wax || {};
+
+// Request
+// -------
+// Request data cache. `callback(data)` where `data` is the response data.
+wax.request = {
+ cache: {},
+ locks: {},
+ promises: {},
+ get: function(url, callback) {
+ // Cache hit.
+ if (this.cache[url]) {
+ return callback(this.cache[url][0], this.cache[url][1]);
+ // Cache miss.
+ } else {
+ this.promises[url] = this.promises[url] || [];
+ this.promises[url].push(callback);
+ // Lock hit.
+ if (this.locks[url]) return;
+ // Request.
+ var that = this;
+ this.locks[url] = true;
+ reqwest({
+ url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid',
+ type: 'jsonp',
+ jsonpCallback: 'callback',
+ success: function(data) {
+ that.locks[url] = false;
+ that.cache[url] = [null, data];
+ for (var i = 0; i < that.promises[url].length; i++) {
+ that.promises[url][i](that.cache[url][0], that.cache[url][1]);
+ }
+ },
+ error: function(err) {
+ that.locks[url] = false;
+ that.cache[url] = [err, null];
+ for (var i = 0; i < that.promises[url].length; i++) {
+ that.promises[url][i](that.cache[url][0], that.cache[url][1]);
+ }
+ }
+ });
+ }
+ }
+};
+// Templating
+// ---------
+wax.template = function(x) {
+ var template = {};
+
+ function urlX(url) {
+ // Data URIs are subject to a bug in Firefox
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=255107
+ // which let them be a vector. But WebKit does 'the right thing'
+ // or at least 'something' about this situation, so we'll tolerate
+ // them.
+ if (/^(https?:\/\/|data:image)/.test(url)) {
+ return url;
+ }
+ }
+
+ function idX(id) {
+ return id;
+ }
+
+ // Clone the data object such that the '__[format]__' key is only
+ // set for this instance of templating.
+ template.format = function(options, data) {
+ var clone = {};
+ for (var key in data) {
+ clone[key] = data[key];
+ }
+ if (options.format) {
+ clone['__' + options.format + '__'] = true;
+ }
+ return html_sanitize(Mustache.to_html(x, clone), urlX, idX);
+ };
+
+ return template;
+};
+if (!wax) var wax = {};
+
+// A wrapper for reqwest jsonp to easily load TileJSON from a URL.
+wax.tilejson = function(url, callback) {
+ reqwest({
+ url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=grid',
+ type: 'jsonp',
+ jsonpCallback: 'callback',
+ success: callback,
+ error: callback
+ });
+};
+var wax = wax || {};
+wax.tooltip = {};
+
+wax.tooltip = function() {
+ var popped = false,
+ animate = false,
+ t = {},
+ tooltips = [],
+ _currentContent,
+ transitionEvent,
+ parent;
+
+ if (document.body.style['-webkit-transition'] !== undefined) {
+ transitionEvent = 'webkitTransitionEnd';
+ } else if (document.body.style.MozTransition !== undefined) {
+ transitionEvent = 'transitionend';
+ }
+
+ // Get the active tooltip for a layer or create a new one if no tooltip exists.
+ // Hide any tooltips on layers underneath this one.
+ function getTooltip(feature) {
+ var tooltip = document.createElement('div');
+ tooltip.className = 'wax-tooltip wax-tooltip-0';
+ tooltip.innerHTML = feature;
+ return tooltip;
+ }
+
+
+ function remove() {
+ if (this.parentNode) this.parentNode.removeChild(this);
+ }
+
+ // Hide a given tooltip.
+ function hide() {
+ var _ct;
+ while (_ct = tooltips.pop()) {
+ if (animate && transitionEvent) {
+ // This code assumes that transform-supporting browsers
+ // also support proper events. IE9 does both.
+ bean.add(_ct, transitionEvent, remove);
+ _ct.className += ' wax-fade';
+ } else {
+ if (_ct.parentNode) _ct.parentNode.removeChild(_ct);
+ }
+ }
+ }
+
+ function on(o) {
+ var content;
+ if (o.e.type === 'mousemove' || !o.e.type) {
+ if (!popped) {
+ content = o.content || o.formatter({ format: 'teaser' }, o.data);
+ if (!content || content == _currentContent) return;
+ hide();
+ parent.style.cursor = 'pointer';
+ tooltips.push(parent.appendChild(getTooltip(content)));
+ _currentContent = content;
+ }
+ } else {
+ content = o.content || o.formatter({ format: 'full' }, o.data);
+ if (!content) {
+ if (o.e.type && o.e.type.match(/touch/)) {
+ // fallback possible
+ content = o.content || o.formatter({ format: 'teaser' }, o.data);
+ }
+ // but if that fails, return just the same.
+ if (!content) return;
+ }
+ hide();
+ parent.style.cursor = 'pointer';
+ var tt = parent.appendChild(getTooltip(content));
+ tt.className += ' wax-popup';
+
+ var close = tt.appendChild(document.createElement('a'));
+ close.href = '#close';
+ close.className = 'close';
+ close.innerHTML = 'Close';
+ popped = true;
+
+ tooltips.push(tt);
+
+ bean.add(close, 'touchstart mousedown', function(e) {
+ e.stop();
+ });
+
+ bean.add(close, 'click touchend', function closeClick(e) {
+ e.stop();
+ hide();
+ popped = false;
+ });
+ }
+ }
+
+ function off() {
+ parent.style.cursor = 'default';
+ _currentContent = null;
+ if (!popped) hide();
+ }
+
+ t.parent = function(x) {
+ if (!arguments.length) return parent;
+ parent = x;
+ return t;
+ };
+
+ t.animate = function(x) {
+ if (!arguments.length) return animate;
+ animate = x;
+ return t;
+ };
+
+ t.events = function() {
+ return {
+ on: on,
+ off: off
+ };
+ };
+
+ return t;
+};
+var wax = wax || {};
+
+// Utils are extracted from other libraries or
+// written from scratch to plug holes in browser compatibility.
+wax.u = {
+ // From Bonzo
+ offset: function(el) {
+ // TODO: window margins
+ //
+ // Okay, so fall back to styles if offsetWidth and height are botched
+ // by Firefox.
+ var width = el.offsetWidth || parseInt(el.style.width, 10),
+ height = el.offsetHeight || parseInt(el.style.height, 10),
+ doc_body = document.body,
+ top = 0,
+ left = 0;
+
+ var calculateOffset = function(el) {
+ if (el === doc_body || el === document.documentElement) return;
+ top += el.offsetTop;
+ left += el.offsetLeft;
+
+ var style = el.style.transform ||
+ el.style.WebkitTransform ||
+ el.style.OTransform ||
+ el.style.MozTransform ||
+ el.style.msTransform;
+
+ if (style) {
+ if (match = style.match(/translate\((.+)px, (.+)px\)/)) {
+ top += parseInt(match[2], 10);
+ left += parseInt(match[1], 10);
+ } else if (match = style.match(/translate3d\((.+)px, (.+)px, (.+)px\)/)) {
+ top += parseInt(match[2], 10);
+ left += parseInt(match[1], 10);
+ } else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) {
+ var pts = match[1].split(',');
+ top += parseInt(pts[13], 10);
+ left += parseInt(pts[12], 10);
+ } else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) {
+ top += parseInt(match[2], 10);
+ left += parseInt(match[1], 10);
+ }
+ }
+ };
+
+ calculateOffset(el);
+
+ try {
+ while (el = el.offsetParent) { calculateOffset(el); }
+ } catch(e) {
+ // Hello, internet explorer.
+ }
+
+ // Offsets from the body
+ top += doc_body.offsetTop;
+ left += doc_body.offsetLeft;
+ // Offsets from the HTML element
+ top += doc_body.parentNode.offsetTop;
+ left += doc_body.parentNode.offsetLeft;
+
+ // Firefox and other weirdos. Similar technique to jQuery's
+ // `doesNotIncludeMarginInBodyOffset`.
+ var htmlComputed = document.defaultView ?
+ window.getComputedStyle(doc_body.parentNode, null) :
+ doc_body.parentNode.currentStyle;
+ if (doc_body.parentNode.offsetTop !==
+ parseInt(htmlComputed.marginTop, 10) &&
+ !isNaN(parseInt(htmlComputed.marginTop, 10))) {
+ top += parseInt(htmlComputed.marginTop, 10);
+ left += parseInt(htmlComputed.marginLeft, 10);
+ }
+
+ return {
+ top: top,
+ left: left,
+ height: height,
+ width: width
+ };
+ },
+
+ '$': function(x) {
+ return (typeof x === 'string') ?
+ document.getElementById(x) :
+ x;
+ },
+
+ // IE doesn't have indexOf
+ indexOf: function(array, item) {
+ var nativeIndexOf = Array.prototype.indexOf;
+ if (array === null) return -1;
+ var i, l;
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
+ for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+ return -1;
+ },
+
+ // From quirksmode: normalize the offset of an event from the top-left
+ // of the page.
+ eventoffset: function(e) {
+ var posx = 0;
+ var posy = 0;
+ if (!e) { e = window.event; }
+ if (e.pageX || e.pageY) {
+ // Good browsers
+ return {
+ x: e.pageX,
+ y: e.pageY
+ };
+ } else if (e.clientX || e.clientY) {
+ // Internet Explorer
+ var doc = document.documentElement, body = document.body;
+ var htmlComputed = document.body.parentNode.currentStyle;
+ var topMargin = parseInt(htmlComputed.marginTop, 10) || 0;
+ var leftMargin = parseInt(htmlComputed.marginLeft, 10) || 0;
+ return {
+ x: e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
+ (doc && doc.clientLeft || body && body.clientLeft || 0) + leftMargin,
+ y: e.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
+ (doc && doc.clientTop || body && body.clientTop || 0) + topMargin
+ };
+ } else if (e.touches && e.touches.length === 1) {
+ // Touch browsers
+ return {
+ x: e.touches[0].pageX,
+ y: e.touches[0].pageY
+ };
+ }
+ },
+
+ // Ripped from underscore.js
+ // Internal function used to implement `_.throttle` and `_.debounce`.
+ limit: function(func, wait, debounce) {
+ var timeout;
+ return function() {
+ var context = this, args = arguments;
+ var throttler = function() {
+ timeout = null;
+ func.apply(context, args);
+ };
+ if (debounce) clearTimeout(timeout);
+ if (debounce || !timeout) timeout = setTimeout(throttler, wait);
+ };
+ },
+
+ // Returns a function, that, when invoked, will only be triggered at most once
+ // during a given window of time.
+ throttle: function(func, wait) {
+ return this.limit(func, wait, false);
+ }
+};
+wax = wax || {};
+wax.leaf = wax.leaf || {};
+
+wax.leaf.hash = function(map) {
+ return wax.hash({
+ getCenterZoom: function () {
+ var center = map.getCenter(),
+ zoom = map.getZoom(),
+ precision = Math.max(
+ 0,
+ Math.ceil(Math.log(zoom) / Math.LN2));
+
+ return [
+ zoom,
+ center.lat.toFixed(precision),
+ center.lng.toFixed(precision)
+ ].join('/');
+ },
+
+ setCenterZoom: function (args) {
+ map.setView(new L.LatLng(args[1], args[2]), args[0]);
+ },
+
+ bindChange: function (fn) {
+ map.on('moveend', fn);
+ },
+
+ unbindChange: function (fn) {
+ map.off('moveend', fn);
+ }
+ });
+};
+wax = wax || {};
+wax.leaf = wax.leaf || {};
+
+wax.leaf.interaction = function() {
+ var dirty = false, _grid, map;
+
+ function setdirty() { dirty = true; }
+
+ function grid() {
+ // TODO: don't build for tiles outside of viewport
+ // Touch interaction leads to intermediate
+ //var zoomLayer = map.createOrGetLayer(Math.round(map.getZoom())); //?what is this doing?
+ // Calculate a tile grid and cache it, by using the `.tiles`
+ // element on this map.
+ if (!dirty && _grid) {
+ return _grid;
+ } else {
+ return (_grid = (function(layers) {
+ var o = [];
+ for (var layerId in layers) {
+ // This only supports tiled layers
+ if (layers[layerId]._tiles) {
+ for (var tile in layers[layerId]._tiles) {
+ var offset = wax.u.offset(layers[layerId]._tiles[tile]);
+ o.push([offset.top, offset.left, layers[layerId]._tiles[tile]]);
+ }
+ }
+ }
+ return o;
+ })(map._layers));
+ }
+ }
+
+ function attach(x) {
+ if (!arguments.length) return map;
+ map = x;
+ var l = ['moveend'];
+ for (var i = 0; i < l.length; i++) {
+ map.on(l[i], setdirty);
+ }
+ }
+
+ function detach(x) {
+ if (!arguments.length) return map;
+ map = x;
+ var l = ['moveend'];
+ for (var i = 0; i < l.length; i++) {
+ map.off(l[i], setdirty);
+ }
+ }
+
+ return wax.interaction()
+ .attach(attach)
+ .detach(detach)
+ .parent(function() {
+ return map._container;
+ })
+ .grid(grid);
+};
+wax = wax || {};
+wax.leaf = wax.leaf || {};
+
+// Legend Control
+// --------------
+// The Leaflet version of this control is a very, very
+// light wrapper around the `/lib` code for legends.
+wax.leaf.legend = function(map, tilejson) {
+ tilejson = tilejson || {};
+ var l, // parent legend
+ legend = {};
+
+ legend.add = function() {
+ l = wax.legend()
+ .content(tilejson.legend || '');
+ return this;
+ };
+
+ legend.content = function(x) {
+ if (x) l.content(x.legend || '');
+ };
+
+ legend.element = function() {
+ return l.element();
+ };
+
+ legend.appendTo = function(elem) {
+ wax.u.$(elem).appendChild(l.element());
+ return this;
+ };
+
+ return legend.add();
+};
+wax = wax || {};
+wax.leaf = wax.leaf || {};
+
+wax.leaf.connector = L.TileLayer.extend({
+ initialize: function(options) {
+ options = options || {};
+ options.minZoom = options.minzoom || 0;
+ options.maxZoom = options.maxzoom || 22;
+ L.TileLayer.prototype.initialize.call(this, options.tiles[0], options);
+ }
+});
+
+window.wax = wax
\ No newline at end of file
diff --git a/css/explore.styl b/css/explore.styl
index 0ae5f35..507ea15 100644
--- a/css/explore.styl
+++ b/css/explore.styl
@@ -1,11 +1,9 @@
$open-sans-condensed = "Open Sans Condensed", "Arial Narrow", sans-serif
.explore
- height 100%
.controls
- width 100%
- padding-left 225px
+ margin-left 225px
.classification-buttons
outer-deboss(3px)
@@ -23,23 +21,21 @@ $open-sans-condensed = "Open Sans Condensed", "Arial Narrow", sans-serif
label
@extend #standard-button
font-size 10px
- label:last-child
- margin-right 4px
.animals
width 400px
background $black
position absolute
- top 26px
+ top 130px
display none
z-index 3
- left 745px
+ left 741px
&.active
display block
- &[data-position='right']
- left 879px
+ &[data-index='2']
+ left 875px
[data-animal]
background $green-dark
@@ -66,7 +62,7 @@ $open-sans-condensed = "Open Sans Condensed", "Arial Narrow", sans-serif
&:active
box-shadow 0 1px 0 rgba($black, 0.25) inset
opacity 0.75
-
+
button.species {
@extend #standard-button
font-size 10px
@@ -85,24 +81,9 @@ $open-sans-condensed = "Open Sans Condensed", "Arial Narrow", sans-serif
width: 0;
}
}
-
- .species-select
- background rgba($black, 0.25)
- border-radius 3px
- box-shadow 0 1px 2px rgba($black, 0.5) inset, 0 -1px 0px rgba($white, 0.25) inset
- left -1px
- padding 1px 0px 3px
- margin-right 24px
- position relative
- top -1px
- width 280px
- // display inline-block
- display none
- text-align center
.slider
display inline-block
- margin-right 125px
.ui-slider
position: relative
@@ -128,30 +109,84 @@ $open-sans-condensed = "Open Sans Condensed", "Arial Narrow", sans-serif
position relative
.map-container
- width 100%
- height 100%
- margin 20px 0 0 0
- min-height 700px
+ position fixed
+ top 160px
+ bottom 0
+ left 0
+ right 0
- .dates
+ .dates, .legend
background rgba(0, 0, 0, .1)
- width 400px
+ width 200px
border-radius 3px
color $black
font inherit
font-weight 700
padding 8px 0
- position relative
+ position absolute
top 20px
- right -500px
+ right 20px
text-align center
text-decoration none
vertical-align middle
- display block
+ display table-column
margin 0 auto
font-size 12px
z-index 4
+ .legend
+ width 130px
+ height 36px
+ opacity 0
+ top 60px
+ text-align right
+ padding 6px
+ border-radius 5px
+ font-size 11px
+ font-weight 400
+ z-index 9
+
+ transition all 800ms cubic-bezier(0.770, 0.000, 0.175, 1.000)
+ -webkit-transition-timing-function cubic-bezier(0.770, 0.000, 0.175, 1.000)
+ -moz-transition-timing-function cubic-bezier(0.770, 0.000, 0.175, 1.000)
+ transition-timing-function cubic-bezier(0.770, 0.000, 0.175, 1.000)
+
+ input
+ display none
+
+ input:checked + label
+ border 2px solid #333
+ box-sizing border-box
+
+ label
+ width 12px
+ height 12px
+ border-radius 6px
+ display inline-block
+ background #525B46
+ cursor pointer
+
+ label:last-child
+ background #D32323
+ margin-top 4px
+
+ span:nth-child(2)
+ margin 0 0 10px
+ span.animal-name
+ display inline-block
+ text-align right
+ padding-right 10px
+
+ span.color
+ display inline-block
+ text-align left
+ padding 0 3px
+
+ &:nth-child(2)
+ background #d32323
+ &:last-child
+ background #525b46
+
img.spinner
position absolute
right 20px
diff --git a/css/layout.styl b/css/layout.styl
index 77a3d78..0678b4f 100644
--- a/css/layout.styl
+++ b/css/layout.styl
@@ -8,6 +8,7 @@ html {
body {
margin: 0;
+ height: 100%;
padding: 0 0 100px;
position: relative;
@@ -87,8 +88,8 @@ html {
}
.main {
- position: relative;
z-index: 1;
+ height 100%
> * {
display: none;
diff --git a/public/js/vendor/heatmap-leaflet.js b/public/js/vendor/heatmap-leaflet.js
deleted file mode 100644
index 07dc4fe..0000000
--- a/public/js/vendor/heatmap-leaflet.js
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * heatmap.js 0.2 Leaflet overlay
- *
- * Copyright (c) 2012, Dominik Moritz
- * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
- * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
- *
- * Attribution for snippets: https://gist.github.com/2566567
- */
-
- L.TileLayer.HeatMap = L.TileLayer.Canvas.extend({
- options: {
- // calculate the maximum value on a per view basis instead of global
- // this creates issues when moving the map
- maxPerView: false,
- debug: false
- },
-
- initialize: function (options) {
- L.Util.setOptions(this, options);
-
- this._cache = {
- max: 0,
- bounds: {}
- };
-
- this._data = [];
-
- this.drawTile = function (tile, tilePoint, zoom) {
- var ctx = {
- tile: tile,
- tilePoint: tilePoint,
- zoom: zoom,
- heatmap: tile.heatmap
- };
-
- if (this.options.debug) {
- this._drawDebugInfo(ctx);
- }
- this._draw(ctx);
- };
- },
-
- // Add a dataset to be drawn. You might want to redraw() if you had previeous datasets.
- addData: function(dataset) {
- this._data = dataset;
- this._cache.max = this._calculateMaxValue(dataset);
- },
-
- _createTileProto: function () {
- var proto = this._tileProto = L.DomUtil.create('div', 'leaflet-tile');
-
- var tileSize = this.options.tileSize;
- proto.style.width = tileSize+"px";
- proto.style.height = tileSize+"px";
- proto.width = tileSize;
- proto.height = tileSize;
- },
-
- _createTile: function () {
- var tile = this._tileProto.cloneNode(false);
- tile.onselectstart = tile.onmousemove = L.Util.falseFn;
-
- var options = this.options;
- var config = {
- "radius": options.radius,
- "element": tile,
- "visible": true,
- "opacity": options.opacity * 100,
- "gradient": options.gradient
- };
- tile.heatmap = h337.create(config);
-
- return tile;
- },
-
- _drawDebugInfo: function (ctx) {
- var canvas = L.DomUtil.create('canvas', 'leaflet-tile-debug');
- var tileSize = this.options.tileSize;
- canvas.width = tileSize;
- canvas.height = tileSize;
- ctx['tile'].appendChild(canvas);
- ctx['canvas'] = canvas;
-
- var max = tileSize;
- var g = ctx.canvas.getContext('2d');
- g.strokeStyle = '#000000';
- g.fillStyle = '#FFFF00';
- g.strokeRect(0, 0, max, max);
- g.font = "12px Arial";
- g.fillRect(0, 0, 5, 5);
- g.fillRect(0, max - 5, 5, 5);
- g.fillRect(max - 5, 0, 5, 5);
- g.fillRect(max - 5, max - 5, 5, 5);
- g.fillRect(max / 2 - 5, max / 2 - 5, 10, 10);
- g.strokeText(ctx.tilePoint.x + ' ' + ctx.tilePoint.y + ' ' + ctx.zoom, max / 2 - 30, max / 2 - 10);
-
- this._drawPoint(ctx, [0,0])
- },
-
- _drawPoint: function (ctx, geom) {
- var p = this._tilePoint(ctx, geom);
- var c = ctx.canvas;
- var g = c.getContext('2d');
- g.beginPath();
- g.fillStyle = '#FF0000';
- g.arc(p.x, p.y, 4, 0, Math.PI * 2);
- g.closePath();
- g.fill();
- g.restore();
- },
-
- _tilePoint: function (ctx, coords) {
- // start coords to tile 'space'
- var s = ctx.tilePoint.multiplyBy(this.options.tileSize);
-
- // actual coords to tile 'space'
- var p = this._map.project(new L.LatLng(coords[1], coords[0]));
-
- // point to draw
- var x = Math.round(p.x - s.x);
- var y = Math.round(p.y - s.y);
- return {
- x: x,
- y: y
- };
- },
-
- // checks whether the point is inside a tile
- _isInTile: function(localXY, padding) {
- padding = padding || this.options.radius;
- var bounds = this._cache.bounds[padding];
- if (!bounds) {
- var tileSize = this.options.tileSize;
- var p1 = new L.Point(-padding, -padding); //topLeft
- var p2 = new L.Point(padding+tileSize, padding+tileSize); //bottomRight
- bounds = this._cache.bounds[padding] = new L.Bounds(p1, p2);
- };
- return bounds.contains([localXY.x, localXY.y]);
- },
-
- // get the max value of the dataset
- _getMaxValue: function() {
- if (this.options.maxPerView) {
- var dataset = [];
- var mapBounds = this._map.getBounds();
- this._data.forEach(function(item){
- if (mapBounds.contains([item.lat, item.lon])) {
- dataset.push(item);
- };
- });
-
- return this._calculateMaxValue(dataset)
- } else {
- return this._cache.max;
- }
- },
-
- _calculateMaxValue: function(dataset) {
- array = [];
- dataset.forEach(function(item){
- array.push(item.value || item.count);
- });
- return Math.max.apply(Math, array);
- },
-
- _draw: function(ctx) {
-
- var heatmap = ctx.heatmap
- heatmap.clear();
-
- var pointsInTile = [];
- if (this._data.length > 0) {
- for (var i=0, l=this._data.length; i 0) {
+ self._completeHandlers.shift()(resp)
+ }
+ }
+
+ function success(resp) {
+ var r = resp.responseText
+ if (r) {
+ switch (type) {
+ case 'json':
+ try {
+ resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')')
+ } catch (err) {
+ return error(resp, 'Could not parse JSON in response', err)
+ }
+ break;
+ case 'js':
+ resp = eval(r)
+ break;
+ case 'html':
+ resp = r
+ break;
+ case 'xml':
+ resp = resp.responseXML;
+ break;
+ }
+ }
+
+ self._responseArgs.resp = resp
+ self._fulfilled = true
+ fn(resp)
+ while (self._fulfillmentHandlers.length > 0) {
+ self._fulfillmentHandlers.shift()(resp)
+ }
+
+ complete(resp)
+ }
+
+ function error(resp, msg, t) {
+ self._responseArgs.resp = resp
+ self._responseArgs.msg = msg
+ self._responseArgs.t = t
+ self._erred = true
+ while (self._errorHandlers.length > 0) {
+ self._errorHandlers.shift()(resp, msg, t)
+ }
+ complete(resp)
+ }
+
+ this.request = getRequest(o, success, error)
+ }
+
+ Reqwest.prototype = {
+ abort: function () {
+ this.request.abort()
+ }
+
+ , retry: function () {
+ init.call(this, this.o, this.fn)
+ }
+
+ /**
+ * Small deviation from the Promises A CommonJs specification
+ * http://wiki.commonjs.org/wiki/Promises/A
+ */
+
+ /**
+ * `then` will execute upon successful requests
+ */
+ , then: function (success, fail) {
+ if (this._fulfilled) {
+ success(this._responseArgs.resp)
+ } else if (this._erred) {
+ fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
+ } else {
+ this._fulfillmentHandlers.push(success)
+ this._errorHandlers.push(fail)
+ }
+ return this
+ }
+
+ /**
+ * `always` will execute whether the request succeeds or fails
+ */
+ , always: function (fn) {
+ if (this._fulfilled || this._erred) {
+ fn(this._responseArgs.resp)
+ } else {
+ this._completeHandlers.push(fn)
+ }
+ return this
+ }
+
+ /**
+ * `fail` will execute when the request fails
+ */
+ , fail: function (fn) {
+ if (this._erred) {
+ fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
+ } else {
+ this._errorHandlers.push(fn)
+ }
+ return this
+ }
+ }
+
+ function reqwest(o, fn) {
+ return new Reqwest(o, fn)
+ }
+
+ // normalize newline variants according to spec -> CRLF
+ function normalize(s) {
+ return s ? s.replace(/\r?\n/g, '\r\n') : ''
+ }
+
+ function serial(el, cb) {
+ var n = el.name
+ , t = el.tagName.toLowerCase()
+ , optCb = function (o) {
+ // IE gives value="" even where there is no value attribute
+ // 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
+ if (o && !o.disabled)
+ cb(n, normalize(o.attributes.value && o.attributes.value.specified ? o.value : o.text))
+ }
+
+ // don't serialize elements that are disabled or without a name
+ if (el.disabled || !n) return;
+
+ switch (t) {
+ case 'input':
+ if (!/reset|button|image|file/i.test(el.type)) {
+ var ch = /checkbox/i.test(el.type)
+ , ra = /radio/i.test(el.type)
+ , val = el.value;
+ // WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
+ (!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
+ }
+ break;
+ case 'textarea':
+ cb(n, normalize(el.value))
+ break;
+ case 'select':
+ if (el.type.toLowerCase() === 'select-one') {
+ optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
+ } else {
+ for (var i = 0; el.length && i < el.length; i++) {
+ el.options[i].selected && optCb(el.options[i])
+ }
+ }
+ break;
+ }
+ }
+
+ // collect up all form elements found from the passed argument elements all
+ // the way down to child elements; pass a '