torque/dist/torque.uncompressed.js
2014-03-21 15:57:51 +01:00

4433 lines
124 KiB
JavaScript

(function(exports) {
exports.torque = exports.torque || {};
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function(c) { setTimeout(c, 16); }
var cancelAnimationFrame = window.requestAnimationFrame || window.mozCancelAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function(c) { };
/**
* options:
* animationDuration in seconds
* animationDelay in seconds
*/
function Animator(callback, options) {
if(!options.steps) {
throw new Error("steps option missing")
}
this.options = options;
this.running = false;
this._tick = this._tick.bind(this);
this._t0 = +new Date();
this.callback = callback;
this._time = 0.0;
_.defaults(this.options, {
animationDelay: 0,
maxDelta: 0.2,
loop: true
});
this.rescale();
}
Animator.prototype = {
start: function() {
this.running = true;
requestAnimationFrame(this._tick);
},
isRunning: function() {
return this.running;
},
stop: function() {
this.pause();
this.time(0);
},
// real animation time
time: function(_) {
if (!arguments.length) return this._time;
this._time = _;
var t = this.range(this.domain(this._time));
this.callback(t);
},
toggle: function() {
if (this.running) {
this.pause()
} else {
this.start()
}
},
rescale: function() {
this.domainInv = torque.math.linear(this.options.animationDelay, this.options.animationDelay + this.options.animationDuration);
this.domain = this.domainInv.invert();
this.range = torque.math.linear(0, this.options.steps);
this.rangeInv = this.range.invert();
this.time(this._time);
return this;
},
duration: function(_) {
if (!arguments.length) return this.options.animationDuration;
this.options.animationDuration = _;
if (this.time() > _) {
this.time(0);
}
this.rescale();
return this;
},
steps: function(_) {
this.options.steps = _;
return this.rescale();
},
step: function(s) {
if(arguments.length === 0) return this.range(this.domain(this._time));
this._time = this.domainInv(this.rangeInv(s));
},
pause: function() {
this.running = false;
cancelAnimationFrame(this._tick);
},
_tick: function() {
var t1 = +new Date();
var delta = (t1 - this._t0)*0.001;
// if delta is really big means the tab lost the focus
// at some point, so limit delta change
delta = Math.min(this.options.maxDelta, delta);
this._t0 = t1;
this._time += delta;
this.time(this._time);
if(this.step() >= this.options.steps) {
this._time = 0;
}
if(this.running) {
requestAnimationFrame(this._tick);
}
}
};
exports.torque.Animator = Animator;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
exports.torque = exports.torque || {};
var _torque_reference_latest = {
"version": "1.0.0",
"style": {
"comp-op": {
"css": "comp-op",
"default-value": "src-over",
"default-meaning": "add the current layer on top of other layers",
"doc": "Composite operation. This defines how this layer should behave relative to layers atop or below it.",
"type": [
"src", //
"src-over", //
"dst-over", //
"src-in", //
"dst-in", //
"src-out", //
"dst-out", //
"src-atop", //
"dst-atop", //
"xor", //
"darken", //
"lighten" //
]
}
},
"layer" : {
"buffer-size": {
"default-value": "0",
"type":"float",
"default-meaning": "No buffer will be used",
"doc": "Extra tolerance around the Layer extent (in pixels) used to when querying and (potentially) clipping the layer data during rendering"
},
"-torque-frame-count": {
"css": "-torque-frame-count",
"default-value": "128",
"type":"number",
"default-meaning": "the data is broken into 128 time frames",
"doc": "Number of animation steps/frames used in the animation. If the data contains a fewere number of total frames, the lesser value will be used."
},
"-torque-resolution": {
"css": "-torque-resolution",
"default-value": "2",
"type":"number",
"default-meaning": "",
"doc": "Spatial resolution in pixels. A resolution of 1 means no spatial aggregation of the data. Any other resolution of N results in spatial aggregation into cells of NxN pixels. The value N must be power of 2"
},
"-torque-animation-duration": {
"css": "-torque-animation-duration",
"default-value": "30",
"type":"number",
"default-meaning": "the animation lasts 30 seconds",
"doc": "Animation duration in seconds"
},
"-torque-aggregation-function": {
"css": "-torque-aggregation-function",
"default-value": "count(cartodb_id)",
"type": "string",
"default-meaning": "the value for each cell is the count of points in that cell",
"doc": "A function used to calculate a value from the aggregate data for each cell. See -torque-resolution"
},
"-torque-time-attribute": {
"css": "-torque-time-attribute",
"default-value": "time",
"type": "string",
"default-meaning": "the data column in your table that is of a time based type",
"doc": "The table column that contains the time information used create the animation"
},
"-torque-data-aggregation": {
"css": "-torque-data-aggregation",
"default-value": "linear",
"type": [
"cumulative"
],
"default-meaning": "previous values are discarded",
"doc": "A linear animation will discard previous values while a cumulative animation will accumulate them until it restarts"
}
},
"symbolizers" : {
"*": {
"comp-op": {
"css": "comp-op",
"default-value": "src-over",
"default-meaning": "add the current layer on top of other layers",
"doc": "Composite operation. This defines how this layer should behave relative to layers atop or below it.",
"type": [
"src", //
"src-over", //
"dst-over", //
"src-in", //
"dst-in", //
"src-out", //
"dst-out", //
"src-atop", //
"dst-atop", //
"xor", //
"darken", //
"lighten" //
]
},
"opacity": {
"css": "opacity",
"type": "float",
"doc": "An alpha value for the style (which means an alpha applied to all features in separate buffer and then composited back to main buffer)",
"default-value": 1,
"default-meaning": "no separate buffer will be used and no alpha will be applied to the style after rendering"
}
},
"trail": {
"steps": {
"css": "trail-steps",
"type": "float",
"default-value": 1,
"default-meaning": "no trail steps",
"doc": "How many steps of trails are going to be rendered"
}
},
"polygon": {
"fill": {
"css": "polygon-fill",
"type": "color",
"default-value": "rgba(128,128,128,1)",
"default-meaning": "gray and fully opaque (alpha = 1), same as rgb(128,128,128)",
"doc": "Fill color to assign to a polygon"
},
"fill-opacity": {
"css": "polygon-opacity",
"type": "float",
"doc": "The opacity of the polygon",
"default-value": 1,
"default-meaning": "opaque"
}
},
"line": {
"stroke": {
"css": "line-color",
"default-value": "rgba(0,0,0,1)",
"type": "color",
"default-meaning": "black and fully opaque (alpha = 1), same as rgb(0,0,0)",
"doc": "The color of a drawn line"
},
"stroke-width": {
"css": "line-width",
"default-value": 1,
"type": "float",
"doc": "The width of a line in pixels"
},
"stroke-opacity": {
"css": "line-opacity",
"default-value": 1,
"type": "float",
"default-meaning": "opaque",
"doc": "The opacity of a line"
},
"stroke-linejoin": {
"css": "line-join",
"default-value": "miter",
"type": [
"miter",
"round",
"bevel"
],
"doc": "The behavior of lines when joining"
},
"stroke-linecap": {
"css": "line-cap",
"default-value": "butt",
"type": [
"butt",
"round",
"square"
],
"doc": "The display of line endings"
}
},
"markers": {
"file": {
"css": "marker-file",
"doc": "An SVG file that this marker shows at each placement. If no file is given, the marker will show an ellipse.",
"default-value": "",
"default-meaning": "An ellipse or circle, if width equals height",
"type": "uri"
},
"opacity": {
"css": "marker-opacity",
"doc": "The overall opacity of the marker, if set, overrides both the opacity of both the fill and stroke",
"default-value": 1,
"default-meaning": "The stroke-opacity and fill-opacity will be used",
"type": "float"
},
"fill-opacity": {
"css": "marker-fill-opacity",
"doc": "The fill opacity of the marker",
"default-value": 1,
"default-meaning": "opaque",
"type": "float"
},
"stroke": {
"css": "marker-line-color",
"doc": "The color of the stroke around a marker shape.",
"default-value": "black",
"type": "color"
},
"stroke-width": {
"css": "marker-line-width",
"doc": "The width of the stroke around a marker shape, in pixels. This is positioned on the boundary, so high values can cover the area itself.",
"type": "float"
},
"stroke-opacity": {
"css": "marker-line-opacity",
"default-value": 1,
"default-meaning": "opaque",
"doc": "The opacity of a line",
"type": "float"
},
"fill": {
"css": "marker-fill",
"default-value": "blue",
"doc": "The color of the area of the marker.",
"type": "color"
},
"marker-type": {
"css": "marker-type",
"type": [
"rectangle",
"ellipse"
],
"default-value": "ellipse",
"doc": "The default marker-type. If a SVG file is not given as the marker-file parameter, the renderer provides either an rectangle or an ellipse (a circle if height is equal to width)"
}
},
"point": {
"file": {
"css": "point-file",
"type": "uri",
"required": false,
"default-value": "none",
"doc": "Image file to represent a point"
},
"opacity": {
"css": "point-opacity",
"type": "float",
"default-value": 1.0,
"default-meaning": "Fully opaque",
"doc": "A value from 0 to 1 to control the opacity of the point"
}
}
},
"colors": {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255,235,205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"grey": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50],
"transparent": [0, 0, 0, 0]
}
};
exports.torque['torque-reference'] = {
version: {
latest: _torque_reference_latest,
'1.0.0': _torque_reference_latest
}
}
})(typeof exports === "undefined" ? this : exports);
//
// common functionallity for torque layers
//
(function(exports) {
function TorqueLayer() {}
TorqueLayer.prototype = {
};
TorqueLayer.optionsFromLayer = function(mapConfig) {
var opts = {};
if (!mapConfig) return opts;
var attrs = {
'buffer-size': 'buffer-size',
'-torque-frame-count': 'steps',
'-torque-resolution': 'resolution',
'-torque-animation-duration': 'animationDuration',
'-torque-aggregation-function': 'countby',
'-torque-time-attribute': 'column',
'-torque-data-aggregation': 'data_aggregation'
};
for (var i in attrs) {
var v = mapConfig.eval(i);
if (v !== undefined) {
var a = attrs[i];
opts[a] = v;
}
}
return opts;
};
TorqueLayer.optionsFromCartoCSS = function(cartocss) {
var shader = new carto.RendererJS().render(cartocss);
var mapConfig = shader.findLayer({ name: 'Map' });
return TorqueLayer.optionsFromLayer(mapConfig);
};
exports.torque.common = torque.common || {};
exports.torque.common.TorqueLayer = TorqueLayer;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
exports.torque = exports.torque || {};
var Event = {};
Event.on = function(evt, callback) {
var cb = this._evt_callbacks = this._evt_callbacks || {};
var l = cb[evt] || (cb[evt] = []);
l.push(callback);
};
Event.trigger = function(evt) {
var c = this._evt_callbacks && this._evt_callbacks[evt];
for(var i = 0; c && i < c.length; ++i) {
c[i].apply(this, Array.prototype.slice.call(arguments, 1));
}
};
Event.fire = Event.trigger;
Event.off = function (evt, callback) {
var c = this._evt_callbacks && this._evt_callbacks[evt];
if (c && !callback) {
delete this._evt_callbacks[evt];
return this;
}
var remove = [];
for(var i = 0; c && i < c.length; ++i) {
if(c[i] === callback) remove.push(i);
}
while((i = remove.pop()) !== undefined) c.splice(i, 1);
};
Event.callbacks = function(evt) {
return (this._evt_callbacks && this._evt_callbacks[evt]) || [];
};
exports.torque.Event = Event;
// types
exports.torque.types = {
Uint8Array: typeof(window['Uint8Array']) !== 'undefined' ? window.Uint8Array : Array,
Uint32Array: typeof(window['Uint32Array']) !== 'undefined' ? window.Uint32Array : Array,
Int32Array: typeof(window['Int32Array']) !== 'undefined' ? window.Int32Array: Array
};
exports.torque.isBrowserSupported = function() {
return !!document.createElement('canvas');
};
})(typeof exports === "undefined" ? this : exports);
if (typeof module !== "undefined") {
module.exports = {
cartocss_reference: require('./cartocss_reference').torque['torque-reference']
};
}
(function(exports) {
exports.torque = exports.torque || {};
function clamp(a, b) {
return function(t) {
return Math.max(Math.min(t, b), a);
};
}
function invLinear(a, b) {
var c = clamp(0, 1.0);
return function(t) {
return c((t - a)/(b - a));
};
}
function linear(a, b) {
var c = clamp(a, b);
function _linear(t) {
return c(a*(1.0 - t) + t*b);
}
_linear.invert = function() {
return invLinear(a, b);
};
return _linear;
}
exports.torque.math = {
clamp: clamp,
linear: linear,
invLinear: invLinear
};
})(typeof exports === "undefined" ? this : exports);
/*
# metrics profiler
## timing
```
var timer = Profiler.metric('resource:load')
time.start();
...
time.end();
```
## counters
```
var counter = Profiler.metric('requests')
counter.inc(); // 1
counter.inc(10); // 11
counter.dec() // 10
counter.dec(10) // 0
```
## Calls per second
```
var fps = Profiler.metric('fps')
function render() {
fps.mark();
}
```
*/
(function(exports) {
var MAX_HISTORY = 1024;
function Profiler() {}
Profiler.metrics = {};
Profiler.get = function(name) {
return Profiler.metrics[name] || {
max: 0,
min: Number.MAX_VALUE,
avg: 0,
total: 0,
count: 0,
history: typeof(Float32Array) !== 'undefined' ? new Float32Array(MAX_HISTORY) : []
};
};
Profiler.new_value = function (name, value) {
var t = Profiler.metrics[name] = Profiler.get(name);
t.max = Math.max(t.max, value);
t.min = Math.min(t.min, value);
t.total += value;
++t.count;
t.avg = t.total / t.count;
t.history[t.count%MAX_HISTORY] = value;
};
Profiler.print_stats = function () {
for (k in Profiler.metrics) {
var t = Profiler.metrics[k];
console.log(" === " + k + " === ");
console.log(" max: " + t.max);
console.log(" min: " + t.min);
console.log(" avg: " + t.avg);
console.log(" count: " + t.count);
console.log(" total: " + t.total);
}
};
function Metric(name) {
this.t0 = null;
this.name = name;
this.count = 0;
}
Metric.prototype = {
//
// start a time measurement
//
start: function() {
this.t0 = +new Date();
return this;
},
// elapsed time since start was called
_elapsed: function() {
return +new Date() - this.t0;
},
//
// finish a time measurement and register it
// ``start`` should be called first, if not this
// function does not take effect
//
end: function() {
if (this.t0 !== null) {
Profiler.new_value(this.name, this._elapsed());
this.t0 = null;
}
},
//
// increments the value
// qty: how many, default = 1
//
inc: function(qty) {
qty = qty === undefined ? 1: qty;
Profiler.new_value(this.name, Profiler.get(this.name).count + (qty ? qty: 0));
},
//
// decrements the value
// qty: how many, default = 1
//
dec: function(qty) {
qty = qty === undefined ? 1: qty;
this.inc(-qty);
},
//
// measures how many times per second this function is called
//
mark: function() {
++this.count;
if(this.t0 === null) {
this.start();
return;
}
var elapsed = this._elapsed();
if(elapsed > 1) {
Profiler.new_value(this.name, this.count);
this.count = 0;
this.start();
}
}
};
Profiler.metric = function(name) {
return new Metric(name);
};
exports.Profiler = Profiler;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
var torque = exports.torque = exports.torque || {};
var providers = exports.torque.providers = exports.torque.providers || {};
var Uint8Array = torque.types.Uint8Array;
var Int32Array = torque.types.Int32Array;
var Uint32Array = torque.types.Uint32Array;
// format('hello, {0}', 'rambo') -> "hello, rambo"
function format(str) {
for(var i = 1; i < arguments.length; ++i) {
var attrs = arguments[i];
for(var attr in attrs) {
str = str.replace(RegExp('\\{' + attr + '\\}', 'g'), attrs[attr]);
}
}
return str;
}
var json = function (options) {
this._ready = false;
this._tileQueue = [];
this.options = options;
this.options.is_time = this.options.is_time === undefined ? true: this.options.is_time;
this.options.tiler_protocol = options.tiler_protocol || 'http';
this.options.tiler_domain = options.tiler_domain || 'cartodb.com';
this.options.tiler_port = options.tiler_port || 80;
if (this.options.data_aggregation) {
this.options.cumulative = this.options.data_aggregation === 'cumulative';
}
// check options
if (options.resolution === undefined ) throw new Error("resolution should be provided");
if (options.steps === undefined ) throw new Error("steps should be provided");
if(options.start === undefined) {
this._fetchKeySpan();
} else {
this._setReady(true);
}
};
json.prototype = {
/**
* return the torque tile encoded in an efficient javascript
* structure:
* {
* x:Uint8Array x coordinates in tile reference system, normally from 0-255
* y:Uint8Array y coordinates in tile reference system
* Index: Array index to the properties
* }
*/
proccessTile: function(rows, coord, zoom) {
var r;
var x = new Uint8Array(rows.length);
var y = new Uint8Array(rows.length);
var prof_mem = Profiler.metric('ProviderJSON:mem');
var prof_point_count = Profiler.metric('ProviderJSON:point_count');
var prof_process_time = Profiler.metric('ProviderJSON:process_time').start()
// count number of dates
var dates = 0;
var maxDateSlots = -1;
for (r = 0; r < rows.length; ++r) {
var row = rows[r];
dates += row.dates__uint16.length;
for(var d = 0; d < row.dates__uint16.length; ++d) {
maxDateSlots = Math.max(maxDateSlots, row.dates__uint16[d]);
}
}
if(this.options.cumulative) {
dates = (1 + maxDateSlots) * rows.length;
}
var type = this.options.cumulative ? Uint32Array: Uint8Array;
// reserve memory for all the dates
var timeIndex = new Int32Array(maxDateSlots + 1); //index-size
var timeCount = new Int32Array(maxDateSlots + 1);
var renderData = new (this.options.valueDataType || type)(dates);
var renderDataPos = new Uint32Array(dates);
prof_mem.inc(
4 * maxDateSlots + // timeIndex
4 * maxDateSlots + // timeCount
dates + //renderData
dates * 4
); //renderDataPos
prof_point_count.inc(rows.length);
var rowsPerSlot = {};
// precache pixel positions
for (var r = 0; r < rows.length; ++r) {
var row = rows[r];
x[r] = row.x__uint8 * this.options.resolution;
// fix value when it's in the tile EDGE
// TODO: this should be fixed in SQL query
if (row.y__uint8 === -1) {
y[r] = 0;
} else {
y[r] = row.y__uint8 * this.options.resolution;
}
var dates = row.dates__uint16;
var vals = row.vals__uint8;
if (!this.options.cumulative) {
for (var j = 0, len = dates.length; j < len; ++j) {
var rr = rowsPerSlot[dates[j]] || (rowsPerSlot[dates[j]] = []);
if(this.options.cumulative) {
vals[j] += prev_val;
}
prev_val = vals[j];
rr.push([r, vals[j]]);
}
} else {
var valByDate = {}
for (var j = 0, len = dates.length; j < len; ++j) {
valByDate[dates[j]] = vals[j];
}
var accum = 0;
// extend the latest to the end
for (var j = dates[0]; j <= maxDateSlots; ++j) {
var rr = rowsPerSlot[j] || (rowsPerSlot[j] = []);
var v = valByDate[j];
if (v) {
accum += v;
}
rr.push([r, accum]);
}
/*var lastDateSlot = dates[dates.length - 1];
for (var j = lastDateSlot + 1; j <= maxDateSlots; ++j) {
var rr = rowsPerSlot[j] || (rowsPerSlot[j] = []);
rr.push([r, prev_val]);
}
*/
}
}
// for each timeslot search active buckets
var renderDataIndex = 0;
var timeSlotIndex = 0;
var i = 0;
for(var i = 0; i <= maxDateSlots; ++i) {
var c = 0;
var slotRows = rowsPerSlot[i]
if(slotRows) {
for (var r = 0; r < slotRows.length; ++r) {
var rr = slotRows[r];
++c;
renderDataPos[renderDataIndex] = rr[0]
renderData[renderDataIndex] = rr[1];
++renderDataIndex;
}
}
timeIndex[i] = timeSlotIndex;
timeCount[i] = c;
timeSlotIndex += c;
}
prof_process_time.end();
return {
x: x,
y: y,
z: zoom,
coord: {
x: coord.x,
y: coord.y,
z: zoom
},
timeCount: timeCount,
timeIndex: timeIndex,
renderDataPos: renderDataPos,
renderData: renderData,
maxDate: maxDateSlots
};
},
_host: function() {
var opts = this.options;
var port = opts.sql_api_port;
var domain = ((opts.user_name || opts.user) + '.' + (opts.sql_api_domain || 'cartodb.com')) + (port ? ':' + port: '');
var protocol = opts.sql_api_protocol || 'http';
return this.options.url || protocol + '://' + domain + '/api/v2/sql';
},
url: function(subhost) {
var opts = this.options;
var protocol = opts.sql_api_protocol || 'http';
if (!this.options.cdn_url) {
return this._host();
}
var h = protocol+ "://";
if (subhost) {
h += subhost + ".";
}
var cdn_host = opts.cdn_url;
if(!cdn_host.http && !cdn_host.https) {
throw new Error("cdn_host should contain http and/or https entries");
}
h += cdn_host[protocol] + "/" + (opts.user_name || opts.user) + '/api/v2/sql';
return h;
},
_hash: function(str) {
var hash = 0;
if (!str || str.length == 0) return hash;
for (var i = 0, l = str.length; i < l; ++i) {
hash = (( (hash << 5 ) - hash ) + str.charCodeAt(i)) | 0;
}
return hash;
},
_extraParams: function() {
if (this.options.extra_params) {
var p = [];
for(var k in this.options.extra_params) {
var v = this.options.extra_params[k];
if (v) {
p.push(k + "=" + encodeURIComponent(v));
}
}
return p.join('&');
}
return null;
},
isHttps: function() {
return this.options.sql_api_protocol && this.options.sql_api_protocol === 'https';
},
// execute actual query
sql: function(sql, callback, options) {
options = options || {};
var subdomains = this.options.subdomains || '0123';
if(this.isHttps()) {
subdomains = [null]; // no subdomain
}
var url;
if (options.no_cdn) {
url = this._host();
} else {
url = this.url(subdomains[Math.abs(this._hash(sql))%subdomains.length]);
}
var extra = this._extraParams();
torque.net.get( url + "?q=" + encodeURIComponent(sql) + (extra ? "&" + extra: ''), function (data) {
if(options.parseJSON) {
data = JSON.parse(data && data.responseText);
}
callback && callback(data);
});
},
getTileData: function(coord, zoom, callback) {
if(!this._ready) {
this._tileQueue.push([coord, zoom, callback]);
} else {
this._getTileData(coord, zoom, callback);
}
},
_setReady: function(ready) {
this._ready = true;
this._processQueue();
this.options.ready && this.options.ready();
},
_processQueue: function() {
var item;
while (item = this._tileQueue.pop()) {
this._getTileData.apply(this, item);
}
},
/**
* `coord` object like {x : tilex, y: tiley }
* `zoom` quadtree zoom level
*/
_getTileData: function(coord, zoom, callback) {
var prof_fetch_time = Profiler.metric('ProviderJSON:tile_fetch_time').start()
this.table = this.options.table;
var numTiles = 1 << zoom;
var column_conv = this.options.column;
if(this.options.is_time) {
column_conv = format("date_part('epoch', {column})", this.options);
}
var sql = "" +
"WITH " +
"par AS (" +
" SELECT CDB_XYZ_Resolution({zoom})*{resolution} as res" +
", 256/{resolution} as tile_size" +
", CDB_XYZ_Extent({x}, {y}, {zoom}) as ext " +
")," +
"cte AS ( "+
" SELECT ST_SnapToGrid(i.the_geom_webmercator, p.res) g" +
", {countby} c" +
", floor(({column_conv} - {start})/{step}) d" +
" FROM ({_sql}) i, par p " +
" WHERE i.the_geom_webmercator && p.ext " +
" GROUP BY g, d" +
") " +
"" +
"SELECT (st_x(g)-st_xmin(p.ext))/p.res x__uint8, " +
" (st_y(g)-st_ymin(p.ext))/p.res y__uint8," +
" array_agg(c) vals__uint8," +
" array_agg(d) dates__uint16" +
// the tile_size where are needed because the overlaps query in cte subquery includes the points
// in the left and bottom borders of the tile
" FROM cte, par p where (st_y(g)-st_ymin(p.ext))/p.res < tile_size and (st_x(g)-st_xmin(p.ext))/p.res < tile_size GROUP BY x__uint8, y__uint8";
var query = format(sql, this.options, {
zoom: zoom,
x: coord.x,
y: coord.y,
column_conv: column_conv,
_sql: this.getSQL()
});
var self = this;
this.sql(query, function (data) {
if (data) {
var rows = JSON.parse(data.responseText).rows;
callback(self.proccessTile(rows, coord, zoom));
} else {
callback(null);
}
prof_fetch_time.end();
});
},
getKeySpan: function() {
return {
start: this.options.start * 1000,
end: this.options.end * 1000,
step: this.options.step,
steps: this.options.steps,
columnType: this.options.is_time ? 'date': 'number'
};
},
setColumn: function(column, isTime) {
this.options.column = column;
this.options.is_time = isTime === undefined ? true: false;
this.reload();
},
setResolution: function(res) {
this.options.resolution = res;
},
// return true if tiles has been changed
setOptions: function(opt) {
var refresh = false;
if(opt.resolution !== undefined && opt.resolution !== this.options.resolution) {
this.options.resolution = opt.resolution;
refresh = true;
}
if(opt.steps !== undefined && opt.steps !== this.options.steps) {
this.setSteps(opt.steps, { silent: true });
refresh = true;
}
if(opt.column !== undefined && opt.column !== this.options.column) {
this.options.column = opt.column;
refresh = true;
}
if(opt.countby !== undefined && opt.countby !== this.options.countby) {
this.options.countby = opt.countby;
refresh = true;
}
if(opt.data_aggregation !== undefined) {
var c = opt.data_aggregation === 'cumulative';
if (this.options.cumulative !== c) {
this.options.cumulative = c;
refresh = true;
}
}
if (refresh) this.reload();
return refresh;
},
reload: function() {
this._ready = false;
this._fetchKeySpan();
},
setSQL: function(sql) {
if (this.options.sql != sql) {
this.options.sql = sql;
this.reload();
}
},
getSteps: function() {
return Math.min(this.options.steps, this.options.data_steps);
},
setSteps: function(steps, opt) {
opt = opt || {};
if (this.options.steps !== steps) {
this.options.steps = steps;
this.options.step = (this.options.end - this.options.start)/this.getSteps();
this.options.step = this.options.step || 1;
if (!opt.silent) this.reload();
}
},
getBounds: function() {
return this.options.bounds;
},
getSQL: function() {
return this.options.sql || "select * from " + this.options.table;
},
_tilerHost: function() {
var opts = this.options;
var user = (opts.user_name || opts.user);
return opts.tiler_protocol +
"://" + (user ? user + "." : "") +
opts.tiler_domain +
((opts.tiler_port != "") ? (":" + opts.tiler_port) : "");
},
_fetchUpdateAt: function(callback) {
var self = this;
var layergroup = {
"version": "1.0.1",
"stat_tag": this.options.stat_tag || 'torque',
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer {}",
"sql": this.getSQL()
}
}]
};
var url = this._tilerHost() + "/tiles/layergroup";
var extra = this._extraParams();
// tiler needs map_key instead of api_key
// so replace it
if (extra) {
extra = extra.replace('api_key=', 'map_key=');
}
url = url +
"?config=" + encodeURIComponent(JSON.stringify(layergroup)) +
"&callback=?" + (extra ? "&" + extra: '');
torque.net.jsonp(url, function (data) {
var query = format("select * from ({sql}) __torque_wrap_sql limit 0", { sql: self.getSQL() });
self.sql(query, function (queryData) {
if (data) {
callback({
updated_at: data.last_updated,
fields: queryData.fields
});
}
}, { parseJSON: true });
});
},
//
// the data range could be set by the user though ``start``
// option. It can be fecthed from the table when the start
// is not specified.
//
_fetchKeySpan: function() {
var self = this;
var max_col, min_col, max_tmpl, min_tmpl;
this._fetchUpdateAt(function(data) {
if (!data) return;
self.options.extra_params = self.options.extra_params || {};
self.options.extra_params.last_updated = data.updated_at || 0;
self.options.extra_params.cache_policy = 'persist';
self.options.is_time = data.fields[self.options.column].type === 'date';
var column_conv = self.options.column;
if (self.options.is_time){
max_tmpl = "date_part('epoch', max({column}))";
min_tmpl = "date_part('epoch', min({column}))";
column_conv = format("date_part('epoch', {column})", self.options);
} else {
max_tmpl = "max({column})";
min_tmpl = "min({column})";
}
max_col = format(max_tmpl, { column: self.options.column });
min_col = format(min_tmpl, { column: self.options.column });
/*var sql_stats = "" +
"WITH summary_groups as ( " +
"WITH summary as ( " +
"select (row_number() over (order by __time_col asc nulls last)+1)/2 as rownum, __time_col " +
"from (select *, {column} as __time_col from ({sql}) __s) __torque_wrap_sql " +
"order by __time_col asc " +
") " +
"SELECT " +
"max(__time_col) OVER(PARTITION BY rownum) - " +
"min(__time_col) OVER(PARTITION BY rownum) diff " +
"FROM summary " +
"), subq as ( " +
" SELECT " +
"st_xmax(st_envelope(st_collect(the_geom))) xmax, " +
"st_ymax(st_envelope(st_collect(the_geom))) ymax, " +
"st_xmin(st_envelope(st_collect(the_geom))) xmin, " +
"st_ymin(st_envelope(st_collect(the_geom))) ymin, " +
"{max_col} max, " +
"{min_col} min FROM ({sql}) __torque_wrap_sql " +
")" +
"SELECT " +
"xmax, xmin, ymax, ymin, a.max as max_date, a.min as min_date, " +
"avg(diff) as diffavg," +
"(a.max - a.min)/avg(diff) as num_steps " +
"FROM summary_groups, subq a " +
"WHERE diff > 0 group by xmax, xmin, ymax, ymin, max_date, min_date";
*/
var sql_stats = " SELECT " +
"st_xmax(st_envelope(st_collect(the_geom))) xmax, " +
"st_ymax(st_envelope(st_collect(the_geom))) ymax, " +
"st_xmin(st_envelope(st_collect(the_geom))) xmin, " +
"st_ymin(st_envelope(st_collect(the_geom))) ymin, " +
"count(*) as num_steps, " +
"{max_col} max_date, " +
"{min_col} min_date FROM ({sql}) __torque_wrap_sql ";
var sql = format(sql_stats, {
max_col: max_col,
min_col: min_col,
column: column_conv,
sql: self.getSQL()
});
self.sql(sql, function(data) {
//TODO: manage bounds
data = data.rows[0];
self.options.start = data.min_date;
self.options.end = data.max_date;
self.options.step = (data.max_date - data.min_date)/Math.min(self.options.steps, data.num_steps>>0);
self.options.data_steps = data.num_steps >> 0;
// step can't be 0
self.options.step = self.options.step || 1;
self.options.bounds = [
[data.ymin, data.xmin],
[data.ymax, data.xmax]
];
self._setReady(true);
}, { parseJSON: true, no_cdn: true });
}, { parseJSON: true, no_cdn: true})
}
};
torque.providers.json = json;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
var torque = exports.torque = exports.torque || {};
var providers = exports.torque.providers = exports.torque.providers || {};
var Uint8Array = torque.types.Uint8Array;
var Int32Array = torque.types.Int32Array;
var Uint32Array = torque.types.Uint32Array;
// format('hello, {0}', 'rambo') -> "hello, rambo"
function format(str, attrs) {
for(var i = 1; i < arguments.length; ++i) {
var attrs = arguments[i];
for(var attr in attrs) {
str = str.replace(RegExp('\\{' + attr + '\\}', 'g'), attrs[attr]);
}
}
return str;
}
var json = function (options) {
// check options
this.options = options;
};
json.prototype = {
//
// return the data aggregated by key:
// {
// key0: 12,
// key1: 32
// key2: 25
// }
//
aggregateByKey: function(rows) {
function getKeys(row) {
var HEADER_SIZE = 3;
var valuesCount = row.data[2];
var keys = {};
for (var s = 0; s < valuesCount; ++s) {
keys[row.data[HEADER_SIZE + s]] = row.data[HEADER_SIZE + valuesCount + s];
}
return keys;
}
var keys = {};
for (r = 0; r < rows.length; ++r) {
var rowKeys = getKeys(rows[r]);
for(var k in rowKeys) {
keys[k] = keys[k] || 0;
keys[k] += rowKeys[k];
}
}
return keys;
},
/**
*
*/
proccessTile: function(rows, coord, zoom) {
var r;
var x = new Uint8Array(rows.length);
var y = new Uint8Array(rows.length);
var self = this;
// decode into a javascript strcuture the array
function decode_row(row) {
var HEADER_SIZE = 3;
var o = {
x: row.data[0] * self.options.resolution,
y: row.data[1] * self.options.resolution,
valuesCount: row.data[2],
times: [],
values: []
};
for (var s = 0; s < o.valuesCount; ++s) {
o.times.push(row.data[HEADER_SIZE + s]);
o.values.push(row.data[HEADER_SIZE + o.valuesCount + s]);
}
if(self.options.cumulative) {
for (var s = 1; s < o.valuesCount; ++s) {
o.values[s] += o.values[s - 1];
}
}
return o
}
// decode all the rows
for (r = 0; r < rows.length; ++r) {
rows[r] = decode_row(rows[r]);
}
// count number of dates
var dates = 0;
var maxDateSlots = 0;
for (r = 0; r < rows.length; ++r) {
var row = rows[r];
dates += row.times.length;
for(var d = 0; d < row.times.length; ++d) {
maxDateSlots = Math.max(maxDateSlots, row.times[d]);
}
}
// reserve memory for all the dates
var timeIndex = new Int32Array(maxDateSlots + 1); //index-size
var timeCount = new Int32Array(maxDateSlots + 1);
var renderData = new (this.options.valueDataType || Uint8Array)(dates);
var renderDataPos = new Uint32Array(dates);
var rowsPerSlot = {};
// precache pixel positions
for (var r = 0; r < rows.length; ++r) {
var row = rows[r];
x[r] = row.x;
y[r] = row.y;
var dates = row.times;
var vals = row.values;
for (var j = 0, len = dates.length; j < len; ++j) {
var rr = rowsPerSlot[dates[j]] || (rowsPerSlot[dates[j]] = []);
rr.push([r, vals[j]]);
}
}
// for each timeslot search active buckets
var renderDataIndex = 0;
var timeSlotIndex = 0;
var i = 0;
for(var i = 0; i <= maxDateSlots; ++i) {
var c = 0;
var slotRows = rowsPerSlot[i]
if(slotRows) {
for (var r = 0; r < slotRows.length; ++r) {
var rr = slotRows[r];
++c;
renderDataPos[renderDataIndex] = rr[0]
renderData[renderDataIndex] = rr[1];
++renderDataIndex;
}
}
timeIndex[i] = timeSlotIndex;
timeCount[i] = c;
timeSlotIndex += c;
}
return {
x: x,
y: y,
coord: {
x: coord.x,
y: coord.y,
z: zoom
},
timeCount: timeCount,
timeIndex: timeIndex,
renderDataPos: renderDataPos,
renderData: renderData
};
},
url: function() {
return this.options.url;
},
tileUrl: function(coord, zoom) {
var template = this.url();
var s = (this.options.subdomains || 'abcd')[(coord.x + coord.y + zoom) % 4];
return template
.replace('{x}', coord.x)
.replace('{y}', coord.y)
.replace('{z}', zoom)
.replace('{s}', s);
},
getTile: function(coord, zoom, callback) {
var template = this.tileUrl(coord, zoom);
var self = this;
var fetchTime = Profiler.metric('jsonarray:fetch time');
fetchTime.start();
torque.net.get(template, function (data) {
fetchTime.end();
if(data) {
data = JSON.parse(data.responseText);
}
callback(data);
});
},
/**
* `coord` object like {x : tilex, y: tiley }
* `zoom` quadtree zoom level
*/
getTileData: function(coord, zoom, callback) {
var template = this.tileUrl(coord, zoom);
var self = this;
var fetchTime = Profiler.metric('jsonarray:fetch time');
fetchTime.start();
torque.net.get(template, function (data) {
fetchTime.end();
var processed = null;
var processingTime = Profiler.metric('jsonarray:processing time');
var parsingTime = Profiler.metric('jsonarray:parsing time');
try {
processingTime.start();
parsingTime.start();
var rows = JSON.parse(data.responseText || data.response).rows;
parsingTime.end();
processed = self.proccessTile(rows, coord, zoom);
processingTime.end();
} catch(e) {
console.error("problem parsing JSON on ", coord, zoom);
}
callback(processed);
});
}
};
torque.providers.JsonArray = json
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
var torque = exports.torque = exports.torque || {};
var providers = exports.torque.providers = exports.torque.providers || {};
var Uint8Array = torque.types.Uint8Array;
var Int32Array = torque.types.Int32Array;
var Uint32Array = torque.types.Uint32Array;
// format('hello, {0}', 'rambo') -> "hello, rambo"
function format(str) {
for(var i = 1; i < arguments.length; ++i) {
var attrs = arguments[i];
for(var attr in attrs) {
str = str.replace(RegExp('\\{' + attr + '\\}', 'g'), attrs[attr]);
}
}
return str;
}
var json = function (options) {
this._ready = false;
this._tileQueue = [];
this.options = options;
this.options.is_time = this.options.is_time === undefined ? true: this.options.is_time;
this.options.tiler_protocol = options.tiler_protocol || 'http';
this.options.tiler_domain = options.tiler_domain || 'cartodb.com';
this.options.tiler_port = options.tiler_port || 80;
if (this.options.data_aggregation) {
this.options.cumulative = this.options.data_aggregation === 'cumulative';
}
if (this.options.auth_token) {
var e = this.options.extra_params || (this.options.extra_params = {});
e.auth_token = this.options.auth_token;
}
this._fetchMap();
};
json.prototype = {
/**
* return the torque tile encoded in an efficient javascript
* structure:
* {
* x:Uint8Array x coordinates in tile reference system, normally from 0-255
* y:Uint8Array y coordinates in tile reference system
* Index: Array index to the properties
* }
*/
proccessTile: function(rows, coord, zoom) {
var r;
var x = new Uint8Array(rows.length);
var y = new Uint8Array(rows.length);
var prof_mem = Profiler.metric('torque.provider.windshaft.mem');
var prof_point_count = Profiler.metric('torque.provider.windshaft.points');
var prof_process_time = Profiler.metric('torque.provider.windshaft.process_time').start();
// count number of dates
var dates = 0;
var maxDateSlots = -1;
for (r = 0; r < rows.length; ++r) {
var row = rows[r];
dates += row.dates__uint16.length;
for(var d = 0; d < row.dates__uint16.length; ++d) {
maxDateSlots = Math.max(maxDateSlots, row.dates__uint16[d]);
}
}
if(this.options.cumulative) {
dates = (1 + maxDateSlots) * rows.length;
}
var type = this.options.cumulative ? Uint32Array: Uint8Array;
// reserve memory for all the dates
var timeIndex = new Int32Array(maxDateSlots + 1); //index-size
var timeCount = new Int32Array(maxDateSlots + 1);
var renderData = new (this.options.valueDataType || type)(dates);
var renderDataPos = new Uint32Array(dates);
prof_mem.inc(
4 * maxDateSlots + // timeIndex
4 * maxDateSlots + // timeCount
dates + //renderData
dates * 4
); //renderDataPos
prof_point_count.inc(rows.length);
var rowsPerSlot = {};
// precache pixel positions
for (var r = 0; r < rows.length; ++r) {
var row = rows[r];
x[r] = row.x__uint8 * this.options.resolution;
// fix value when it's in the tile EDGE
// TODO: this should be fixed in SQL query
if (row.y__uint8 === -1) {
y[r] = 0;
} else {
y[r] = row.y__uint8 * this.options.resolution;
}
var dates = row.dates__uint16;
var vals = row.vals__uint8;
if (!this.options.cumulative) {
for (var j = 0, len = dates.length; j < len; ++j) {
var rr = rowsPerSlot[dates[j]] || (rowsPerSlot[dates[j]] = []);
if(this.options.cumulative) {
vals[j] += prev_val;
}
prev_val = vals[j];
rr.push([r, vals[j]]);
}
} else {
var valByDate = {}
for (var j = 0, len = dates.length; j < len; ++j) {
valByDate[dates[j]] = vals[j];
}
var accum = 0;
// extend the latest to the end
for (var j = dates[0]; j <= maxDateSlots; ++j) {
var rr = rowsPerSlot[j] || (rowsPerSlot[j] = []);
var v = valByDate[j];
if (v) {
accum += v;
}
rr.push([r, accum]);
}
/*var lastDateSlot = dates[dates.length - 1];
for (var j = lastDateSlot + 1; j <= maxDateSlots; ++j) {
var rr = rowsPerSlot[j] || (rowsPerSlot[j] = []);
rr.push([r, prev_val]);
}
*/
}
}
// for each timeslot search active buckets
var renderDataIndex = 0;
var timeSlotIndex = 0;
var i = 0;
for(var i = 0; i <= maxDateSlots; ++i) {
var c = 0;
var slotRows = rowsPerSlot[i]
if(slotRows) {
for (var r = 0; r < slotRows.length; ++r) {
var rr = slotRows[r];
++c;
renderDataPos[renderDataIndex] = rr[0]
renderData[renderDataIndex] = rr[1];
++renderDataIndex;
}
}
timeIndex[i] = timeSlotIndex;
timeCount[i] = c;
timeSlotIndex += c;
}
prof_process_time.end();
return {
x: x,
y: y,
z: zoom,
coord: {
x: coord.x,
y: coord.y,
z: zoom
},
timeCount: timeCount,
timeIndex: timeIndex,
renderDataPos: renderDataPos,
renderData: renderData,
maxDate: maxDateSlots
};
},
/*setCartoCSS: function(c) {
this.options.cartocss = c;
},*/
setSteps: function(steps, opt) {
opt = opt || {};
if (this.options.steps !== steps) {
this.options.steps = steps;
this.options.step = (this.options.end - this.options.start)/this.getSteps();
this.options.step = this.options.step || 1;
if (!opt.silent) this.reload();
}
},
setOptions: function(opt) {
var refresh = false;
if(opt.resolution !== undefined && opt.resolution !== this.options.resolution) {
this.options.resolution = opt.resolution;
refresh = true;
}
if(opt.steps !== undefined && opt.steps !== this.options.steps) {
this.setSteps(opt.steps, { silent: true });
refresh = true;
}
if(opt.column !== undefined && opt.column !== this.options.column) {
this.options.column = opt.column;
refresh = true;
}
if(opt.countby !== undefined && opt.countby !== this.options.countby) {
this.options.countby = opt.countby;
refresh = true;
}
if(opt.data_aggregation !== undefined) {
var c = opt.data_aggregation === 'cumulative';
if (this.options.cumulative !== c) {
this.options.cumulative = c;
refresh = true;
}
}
if (refresh) this.reload();
return refresh;
},
_extraParams: function() {
if (this.options.extra_params) {
var p = [];
for(var k in this.options.extra_params) {
var v = this.options.extra_params[k];
if (v) {
p.push(k + "=" + encodeURIComponent(v));
}
}
return p.join('&');
}
return null;
},
getTileData: function(coord, zoom, callback) {
if(!this._ready) {
this._tileQueue.push([coord, zoom, callback]);
} else {
this._getTileData(coord, zoom, callback);
}
},
_setReady: function(ready) {
this._ready = true;
this._processQueue();
this.options.ready && this.options.ready();
},
_processQueue: function() {
var item;
while (item = this._tileQueue.pop()) {
this._getTileData.apply(this, item);
}
},
/**
* `coord` object like {x : tilex, y: tiley }
* `zoom` quadtree zoom level
*/
_getTileData: function(coord, zoom, callback) {
var self = this;
var prof_fetch_time = Profiler.metric('torque.provider.windshaft.tile.fetch').start();
var subdomains = this.options.subdomains || '0123';
var index = Math.abs(coord.x + coord.y) % subdomains.length;
var url = this.templateUrl
.replace('{x}', coord.x)
.replace('{y}', coord.y)
.replace('{z}', zoom)
.replace('{s}', subdomains[index])
var extra = this._extraParams();
torque.net.get( url + (extra ? "?" + extra: ''), function (data) {
prof_fetch_time.end();
if (data && data.responseText) {
var rows = JSON.parse(data.responseText);
callback(self.proccessTile(rows, coord, zoom));
} else {
Profiler.metric('torque.provider.windshaft.tile.error').inc();
callback(null);
}
});
},
getKeySpan: function() {
return {
start: this.options.start,
end: this.options.end,
step: this.options.step,
steps: this.options.steps,
columnType: this.options.column_type
};
},
setColumn: function(column, isTime) {
this.options.column = column;
this.options.is_time = isTime === undefined ? true: false;
this.reload();
},
reload: function() {
this._ready = false;
this._fetchMap();
},
getSteps: function() {
return Math.min(this.options.steps, this.options.data_steps);
},
getBounds: function() {
return this.options.bounds;
},
getSQL: function() {
return this.options.sql || "select * from " + this.options.table;
},
setSQL: function(sql) {
if (this.options.sql != sql) {
this.options.sql = sql;
this.reload();
}
},
_tilerHost: function() {
var opts = this.options;
var user = (opts.user_name || opts.user);
return opts.tiler_protocol +
"://" + (user ? user + "." : "") +
opts.tiler_domain +
((opts.tiler_port != "") ? (":" + opts.tiler_port) : "");
},
url: function() {
var opts = this.options;
var protocol = opts.tiler_protocol || 'http';
if (!this.options.cdn_url) {
return this._tilerHost();
}
var h = protocol + "://"
if (protocol === 'http') {
h += "{s}.";
}
var cdn_host = opts.cdn_url;
if(!cdn_host.http && !cdn_host.https) {
throw new Error("cdn_host should contain http and/or https entries");
}
h += cdn_host[protocol] + "/" + (opts.user_name || opts.user);
return h;
},
_generateCartoCSS: function() {
var attr = {
'-torque-frame-count': this.options.steps,
'-torque-resolution': this.options.resolution,
'-torque-aggregation-function': "'" + this.options.countby + "'",
'-torque-time-attribute': "'" + this.options.column + "'",
'-torque-data-aggregation': this.options.cumulative ? 'cumulative': 'linear',
};
var st = 'Map{';
for (var k in attr) {
st += k + ":" + attr[k] + ";";
}
return st + "}";
},
_fetchMap: function(callback) {
var self = this;
var layergroup = {};
var url = this._tilerHost() + "/api/v1/map";
var named = this.options.named_map;
if(named) {
//tiles/template
url = this._tilerHost() + "/api/v1/map/named/" + named.name + "/jsonp"
//url = this._tilerHost() + "/map/" + named.name + "/jsonp"
} else {
layergroup = {
"version": "1.0.1",
"stat_tag": this.options.stat_tag || 'torque',
"layers": [{
"type": "torque",
"options": {
"cartocss_version": "1.0.0",
"cartocss": this._generateCartoCSS(),
"sql": this.getSQL()
}
}]
};
}
var extra = this._extraParams();
// tiler needs map_key instead of api_key
// so replace it
if (extra) {
extra = extra.replace('api_key=', 'map_key=');
}
url = url +
"?config=" + encodeURIComponent(JSON.stringify(layergroup)) +
"&callback=?" + (extra ? "&" + extra: '');
var map_instance_time = Profiler.metric('torque.provider.windshaft.layergroup.time').start();
torque.net.jsonp(url, function (data) {
map_instance_time.end();
if (data) {
var torque_key = Object.keys(data.metadata.torque)[0]
var opt = data.metadata.torque[torque_key];
for(var k in opt) {
self.options[k] = opt[k];
}
self.templateUrl = self.url() + "/api/v1/map/" + data.layergroupid + "/" + torque_key + "/{z}/{x}/{y}.json.torque";
self._setReady(true);
} else {
Profiler.metric('torque.provider.windshaft.layergroup.error').inc();
}
});
}
};
torque.providers.windshaft = json;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
var torque = exports.torque = exports.torque || {};
torque.net = torque.net || {};
var lastCall = null;
function jsonp(url, callback, options) {
options = options || { timeout: 10000 };
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
// function name
var fnName = 'torque_' + Date.now();
function clean() {
head.removeChild(script);
clearTimeout(timeoutTimer);
delete window[fnName];
}
window[fnName] = function() {
clean();
callback.apply(window, arguments);
};
// timeout for errors
var timeoutTimer = setTimeout(function() {
clean();
callback.call(window, null);
}, options.timeout);
// setup url
url = url.replace('callback=\?', 'callback=' + fnName);
script.type = 'text/javascript';
script.src = url;
script.async = true;
// defer the loading because IE9 loads in the same frame the script
// so Loader._script is null
setTimeout(function() { head.appendChild(script); }, 0);
}
function get(url, callback, options) {
options = options || {
method: 'GET',
data: null,
responseType: 'text'
};
lastCall = { url: url, callback: callback };
var request = XMLHttpRequest;
// from d3.js
if (window.XDomainRequest
&& !("withCredentials" in request)
&& /^(http(s)?:)?\/\//.test(url)) request = XDomainRequest;
var req = new request();
req.open(options.method, url, true);
function respond() {
var status = req.status, result;
var r = options.responseType === 'arraybuffer' ? req.response: req.responseText;
if (!status && r || status >= 200 && status < 300 || status === 304) {
callback(req);
} else {
callback(null);
}
}
"onload" in req
? req.onload = req.onerror = respond
: req.onreadystatechange = function() { req.readyState > 3 && respond(); };
req.onprogress = function() {};
req.responseType = options.responseType; //'arraybuffer';
if (options.data) {
req.setRequestHeader("Content-type", "application/json");
//req.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
req.setRequestHeader("Accept", "*");
}
req.send(options.data);
return req;
}
function post(url, data, callback) {
return get(url, callback, {
data: data,
method: "POST"
});
}
torque.net = {
get: get,
post: post,
jsonp: jsonp,
lastCall: function() { return lastCall; }
};
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
exports.torque = exports.torque || {};
var TAU = Math.PI*2;
function renderPoint(ctx, st) {
ctx.fillStyle = st.fillStyle;
ctx.strokStyle = st.strokStyle;
var pixel_size = st['point-radius'];
// render a circle
// fill
ctx.beginPath();
ctx.arc(0, 0, pixel_size, 0, TAU, true, true);
ctx.closePath();
if (st.fillStyle) {
if (st.fillOpacity) {
ctx.globalAlpha = st.fillOpacity;
}
ctx.fill();
}
// stroke
ctx.globalAlpha = 1.0;
if (st.strokeStyle && st.lineWidth) {
if (st.strokeOpacity) {
ctx.globalAlpha = st.strokeOpacity;
}
if (st.lineWidth) {
ctx.lineWidth = st.lineWidth;
}
ctx.strokeStyle = st.strokeStyle;
// do not render for alpha = 0
if (ctx.globalAlpha > 0) {
ctx.stroke();
}
}
}
function renderRectangle(ctx, st) {
ctx.fillStyle = st.fillStyle;
ctx.strokStyle = st.strokStyle;
var pixel_size = st['point-radius'];
// fill
if (st.fillStyle && st.fillOpacity) {
ctx.globalAlpha = st.fillOpacity;
}
ctx.fillRect(0, 0, pixel_size, pixel_size)
// stroke
ctx.globalAlpha = 1.0;
if (st.strokeStyle && st.lineWidth) {
if (st.strokeOpacity) {
ctx.globalAlpha = st.strokeOpacity;
}
if (st.lineWidth) {
ctx.lineWidth = st.lineWidth;
}
ctx.strokeStyle = st.strokeStyle;
// do not render for alpha = 0
if (ctx.globalAlpha > 0) {
ctx.strokeRect(0, 0, pixel_size, pixel_size);
}
}
}
function renderSprite(ctx, st) {
var img = st['point-file'] || st['marker-file'];
var ratio = img.height/img.width;
var w = st['point-radius'] || img.width;
var h = st['point-radius'] || st['marker-height'] || w*ratio;
ctx.drawImage(img, 0, 0, w, h);
}
exports.torque.cartocss = exports.torque.cartocss || {};
exports.torque.cartocss = {
renderPoint: renderPoint,
renderSprite: renderSprite,
renderRectangle: renderRectangle
};
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
exports.torque = exports.torque || {};
exports.torque.renderer = exports.torque.renderer || {};
var TAU = Math.PI * 2;
var DEFAULT_CARTOCSS = [
'#layer {',
' marker-fill: #662506;',
' marker-width: 4;',
' [value > 1] { marker-fill: #FEE391; }',
' [value > 2] { marker-fill: #FEC44F; }',
' [value > 3] { marker-fill: #FE9929; }',
' [value > 4] { marker-fill: #EC7014; }',
' [value > 5] { marker-fill: #CC4C02; }',
' [value > 6] { marker-fill: #993404; }',
' [value > 7] { marker-fill: #662506; }',
'}'
].join('\n');
//
// this renderer just render points depending of the value
//
function PointRenderer(canvas, options) {
if (!canvas) {
throw new Error("canvas can't be undefined");
}
this.options = options;
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
this._sprites = []; // sprites per layer
this._shader = null;
this.setCartoCSS(this.options.cartocss || DEFAULT_CARTOCSS);
}
PointRenderer.prototype = {
setCanvas: function(canvas) {
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
},
//
// sets the cartocss style to render stuff
//
setCartoCSS: function(cartocss) {
// clean sprites
this.setShader(new carto.RendererJS().render(cartocss));
},
setShader: function(shader) {
// clean sprites
this._sprites = [];
this._shader = shader;
},
clearSpriteCache: function() {
this._sprites = [];
},
//
// generate sprite based on cartocss style
//
generateSprite: function(shader, value, shaderVars) {
var prof = Profiler.metric('torque.renderer.point.generateSprite').start();
var st = shader.getStyle('canvas-2d', {
value: value
}, shaderVars);
var pointSize = st['point-radius'];
if (!pointSize) {
throw new Error("marker-width property should be set");
}
// take into account the exterior ring to calculate the size
var canvasSize = (st.lineWidth || 0) + pointSize*2;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
ctx.width = canvas.width = Math.ceil(canvasSize);
ctx.height = canvas.height = Math.ceil(canvasSize);
ctx.translate(canvasSize/2, canvasSize/2);
if(st['point-file'] || st['marker-file']) {
torque.cartocss.renderSprite(ctx, st);
} else {
var mt = st['marker-type'];
if (mt && mt === 'rectangle') {
torque.cartocss.renderRectangle(ctx, st);
} else {
torque.cartocss.renderPoint(ctx, st);
}
}
prof.end();
return canvas;
},
//
// renders all the layers (and frames for each layer) from cartocss
//
renderTile: function(tile, key) {
var prof = Profiler.metric('torque.renderer.point.renderLayers').start();
var layers = this._shader.getLayers();
for(var i = 0, n = layers.length; i < n; ++i ) {
var layer = layers[i];
if (layer.name() !== "Map") {
var sprites = this._sprites[i] || (this._sprites[i] = {});
// frames for each layer
for(var fr = 0; fr < layer.frames().length; ++fr) {
var frame = layer.frames()[fr];
var fr_sprites = sprites[frame] || (sprites[frame] = []);
this._renderTile(tile, key - frame, frame, fr_sprites, layer);
}
}
}
prof.end();
},
//
// renders a tile in the canvas for key defined in
// the torque tile
//
_renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars) {
if(!this._canvas) return;
var prof = Profiler.metric('torque.renderer.point.renderTile').start();
var ctx = this._ctx;
var blendMode = shader.eval('comp-op') || this.options.blendmode;
if(blendMode) {
ctx.globalCompositeOperation = blendMode;
}
if (this.options.cumulative && key > tile.maxDate) {
//TODO: precache because this tile is not going to change
key = tile.maxDate;
}
var tileMax = this.options.resolution * (256/this.options.resolution - 1)
var activePixels = tile.timeCount[key];
if(activePixels) {
var pixelIndex = tile.timeIndex[key];
for(var p = 0; p < activePixels; ++p) {
var posIdx = tile.renderDataPos[pixelIndex + p];
var c = tile.renderData[pixelIndex + p];
if(c) {
var sp = sprites[c];
if(!sp) {
sp = sprites[c] = this.generateSprite(shader, c, _.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars));
}
//var x = tile.x[posIdx]*res - (sp.width >> 1);
//var y = (256 - res - res*tile.y[posIdx]) - (sp.height >> 1);
var x = tile.x[posIdx]- (sp.width >> 1);
var y = tileMax - tile.y[posIdx]; // flip mercator
ctx.drawImage(sp, x, y - (sp.height >> 1));
}
}
}
prof.end();
},
setBlendMode: function(b) {
this.options.blendmode = b;
}
};
// exports public api
exports.torque.renderer.Point = PointRenderer;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
exports.torque = exports.torque || {};
exports.torque.renderer = exports.torque.renderer || {};
var DEFAULT_CARTOCSS = [
'#layer {',
' polygon-fill: #FFFF00;',
' [value > 10] { polygon-fill: #FFFF00; }',
' [value > 100] { polygon-fill: #FFCC00; }',
' [value > 1000] { polygon-fill: #FE9929; }',
' [value > 10000] { polygon-fill: #FF6600; }',
' [value > 100000] { polygon-fill: #FF3300; }',
'}'
].join('\n');
var TAU = Math.PI * 2;
//
// this renderer just render points depending of the value
//
function RectanbleRenderer(canvas, options) {
this.options = options;
carto.tree.Reference.set(torque['torque-reference']);
this.setCanvas(canvas);
this.setCartoCSS(this.options.cartocss || DEFAULT_CARTOCSS);
}
RectanbleRenderer.prototype = {
//
// sets the cartocss style to render stuff
//
setCartoCSS: function(cartocss) {
this._cartoCssStyle = new carto.RendererJS().render(cartocss);
if(this._cartoCssStyle.getLayers().length > 1) {
throw new Error("only one CartoCSS layer is supported");
}
this._shader = this._cartoCssStyle.getLayers()[0].shader;
},
setCanvas: function(canvas) {
if(!canvas) return;
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
},
accumulate: function(tile, keys) {
var prof = Profiler.metric('RectangleRender:accumulate').start();
var x, y, posIdx, p, k, key, activePixels, pixelIndex;
var res = this.options.resolution;
var s = 256/res;
var accum = new Float32Array(s*s);
if(typeof(keys) !== 'object') {
keys = [keys];
}
for(k = 0; k < keys.length; ++k) {
key = keys[k];
activePixels = tile.timeCount[key];
if(activePixels) {
pixelIndex = tile.timeIndex[key];
for(p = 0; p < activePixels; ++p) {
posIdx = tile.renderDataPos[pixelIndex + p];
x = tile.x[posIdx]/res;
y = tile.y[posIdx]/res;
accum[x*s + y] += tile.renderData[pixelIndex + p];
}
}
}
prof.end();
return accum;
},
renderTileAccum: function(accum, px, py) {
var prof = Profiler.metric('RectangleRender:renderTileAccum').start();
var color, x, y, alpha;
var res = this.options.resolution;
var ctx = this._ctx;
var s = (256/res) | 0;
var s2 = s*s;
var colors = this._colors;
if(this.options.blendmode) {
ctx.globalCompositeOperation = this.options.blendmode;
}
var polygon_alpha = this._shader['polygon-opacity'] || function() { return 1.0; };
for(var i = 0; i < s2; ++i) {
var xy = i;
var value = accum[i];
if(value) {
x = (xy/s) | 0;
y = xy % s;
// by-pass the style generation for improving performance
color = this._shader['polygon-fill']({ value: value }, { zoom: 0 });
ctx.fillStyle = color;
//TODO: each function should have a default value for each
//property defined in the cartocss
alpha = polygon_alpha({ value: value }, { zoom: 0 });
if(alpha === null) {
alpha = 1.0;
}
ctx.globalAlpha = alpha;
ctx.fillRect(x * res, 256 - res - y * res, res, res);
}
}
prof.end();
},
//
// renders a tile in the canvas for key defined in
// the torque tile
//
renderTile: function(tile, key, px, py) {
if(!this._canvas) return;
var res = this.options.resolution;
//var prof = Profiler.get('render').start();
var ctx = this._ctx;
var colors = this._colors;
var activepixels = tile.timeCount[key];
if(activepixels) {
var w = this._canvas.width;
var h = this._canvas.height;
//var imageData = ctx.getImageData(0, 0, w, h);
//var pixels = imageData.data;
var pixelIndex = tile.timeIndex[key];
for(var p = 0; p < activePixels; ++p) {
var posIdx = tile.renderDataPos[pixelIndex + p];
var c = tile.renderData[pixelIndex + p];
if(c) {
var color = colors[Math.min(c, colors.length - 1)];
var x = tile.x[posIdx];// + px;
var y = tile.y[posIdx]; //+ py;
ctx.fillStyle = color;
ctx.fillRect(x, y, res, res);
/*
for(var xx = 0; xx < res; ++xx) {
for(var yy = 0; yy < res; ++yy) {
var idx = 4*((x+xx) + w*(y + yy));
pixels[idx + 0] = color[0];
pixels[idx + 1] = color[1];
pixels[idx + 2] = color[2];
pixels[idx + 3] = color[3];
}
}
*/
}
}
//ctx.putImageData(imageData, 0, 0);
}
//prof.end();
}
};
// exports public api
exports.torque.renderer.Rectangle = RectanbleRenderer;
})(typeof exports === "undefined" ? this : exports);
/**
* @license
* Copyright 2013 Google Inc. All Rights Reserved.
*
* 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 Extends OverlayView to provide a canvas "Layer".
* @author Brendan Kenny
*/
/**
* A map layer that provides a canvas over the slippy map and a callback
* system for efficient animation. Requires canvas and CSS 2D transform
* support.
* @constructor
* @extends google.maps.OverlayView
* @param {CanvasLayerOptions=} opt_options Options to set in this CanvasLayer.
*/
if(typeof(google) !== 'undefined' && typeof(google.maps) !== 'undefined') {
function CanvasLayer(opt_options) {
/**
* If true, canvas is in a map pane and the OverlayView is fully functional.
* See google.maps.OverlayView.onAdd for more information.
* @type {boolean}
* @private
*/
this.isAdded_ = false;
/**
* If true, each update will immediately schedule the next.
* @type {boolean}
* @private
*/
this.isAnimated_ = false;
/**
* The name of the MapPane in which this layer will be displayed.
* @type {string}
* @private
*/
this.paneName_ = CanvasLayer.DEFAULT_PANE_NAME_;
/**
* A user-supplied function called whenever an update is required. Null or
* undefined if a callback is not provided.
* @type {?function=}
* @private
*/
this.updateHandler_ = null;
/**
* A user-supplied function called whenever an update is required and the
* map has been resized since the last update. Null or undefined if a
* callback is not provided.
* @type {?function}
* @private
*/
this.resizeHandler_ = null;
/**
* The LatLng coordinate of the top left of the current view of the map. Will
* be null when this.isAdded_ is false.
* @type {google.maps.LatLng}
* @private
*/
this.topLeft_ = null;
/**
* The map-pan event listener. Will be null when this.isAdded_ is false. Will
* be null when this.isAdded_ is false.
* @type {?function}
* @private
*/
this.centerListener_ = null;
/**
* The map-resize event listener. Will be null when this.isAdded_ is false.
* @type {?function}
* @private
*/
this.resizeListener_ = null;
/**
* If true, the map size has changed and this.resizeHandler_ must be called
* on the next update.
* @type {boolean}
* @private
*/
this.needsResize_ = true;
/**
* A browser-defined id for the currently requested callback. Null when no
* callback is queued.
* @type {?number}
* @private
*/
this.requestAnimationFrameId_ = null;
var canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.pointerEvents = 'none';
/**
* The canvas element.
* @type {!HTMLCanvasElement}
*/
this.canvas = canvas;
/**
* Simple bind for functions with no args for bind-less browsers (Safari).
* @param {Object} thisArg The this value used for the target function.
* @param {function} func The function to be bound.
*/
function simpleBindShim(thisArg, func) {
return function() { func.apply(thisArg); };
}
/**
* A reference to this.repositionCanvas_ with this bound as its this value.
* @type {function}
* @private
*/
this.repositionFunction_ = simpleBindShim(this, this.repositionCanvas_);
/**
* A reference to this.resize_ with this bound as its this value.
* @type {function}
* @private
*/
this.resizeFunction_ = simpleBindShim(this, this.resize_);
/**
* A reference to this.update_ with this bound as its this value.
* @type {function}
* @private
*/
this.requestUpdateFunction_ = simpleBindShim(this, this.update_);
// set provided options, if any
if (opt_options) {
this.setOptions(opt_options);
}
}
CanvasLayer.prototype = new google.maps.OverlayView();
/**
* The default MapPane to contain the canvas.
* @type {string}
* @const
* @private
*/
CanvasLayer.DEFAULT_PANE_NAME_ = 'overlayLayer';
/**
* Transform CSS property name, with vendor prefix if required. If browser
* does not support transforms, property will be ignored.
* @type {string}
* @const
* @private
*/
CanvasLayer.CSS_TRANSFORM_ = (function() {
var div = document.createElement('div');
var transformProps = [
'transform',
'WebkitTransform',
'MozTransform',
'OTransform',
'msTransform'
];
for (var i = 0; i < transformProps.length; i++) {
var prop = transformProps[i];
if (div.style[prop] !== undefined) {
return prop;
}
}
// return unprefixed version by default
return transformProps[0];
})();
/**
* The requestAnimationFrame function, with vendor-prefixed or setTimeout-based
* fallbacks. MUST be called with window as thisArg.
* @type {function}
* @param {function} callback The function to add to the frame request queue.
* @return {number} The browser-defined id for the requested callback.
* @private
*/
CanvasLayer.prototype.requestAnimFrame_ =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return window.setTimeout(callback, 1000 / 60);
};
/**
* The cancelAnimationFrame function, with vendor-prefixed fallback. Does not
* fall back to clearTimeout as some platforms implement requestAnimationFrame
* but not cancelAnimationFrame, and the cost is an extra frame on onRemove.
* MUST be called with window as thisArg.
* @type {function}
* @param {number=} requestId The id of the frame request to cancel.
* @private
*/
CanvasLayer.prototype.cancelAnimFrame_ =
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function(requestId) {};
/**
* Sets any options provided. See CanvasLayerOptions for more information.
* @param {CanvasLayerOptions} options The options to set.
*/
CanvasLayer.prototype.setOptions = function(options) {
if (options.animate !== undefined) {
this.setAnimate(options.animate);
}
if (options.paneName !== undefined) {
this.setPane(options.paneName);
}
if (options.updateHandler !== undefined) {
this.setUpdateHandler(options.updateHandler);
}
if (options.resizeHandler !== undefined) {
this.setResizeHandler(options.resizeHandler);
}
if(options.readyHandler) {
this.readyHandler = options.readyHandler;
}
};
/**
* Set the animated state of the layer. If true, updateHandler will be called
* repeatedly, once per frame. If false, updateHandler will only be called when
* a map property changes that could require the canvas content to be redrawn.
* @param {boolean} animate Whether the canvas is animated.
*/
CanvasLayer.prototype.setAnimate = function(animate) {
this.isAnimated_ = !!animate;
if (this.isAnimated_) {
this.scheduleUpdate();
}
};
/**
* @return {boolean} Whether the canvas is animated.
*/
CanvasLayer.prototype.isAnimated = function() {
return this.isAnimated_;
};
/**
* Set the MapPane in which this layer will be displayed, by name. See
* {@code google.maps.MapPanes} for the panes available.
* @param {string} paneName The name of the desired MapPane.
*/
CanvasLayer.prototype.setPaneName = function(paneName) {
this.paneName_ = paneName;
this.setPane_();
};
/**
* @return {string} The name of the current container pane.
*/
CanvasLayer.prototype.getPaneName = function() {
return this.paneName_;
};
/**
* Adds the canvas to the specified container pane. Since this is guaranteed to
* execute only after onAdd is called, this is when paneName's existence is
* checked (and an error is thrown if it doesn't exist).
* @private
*/
CanvasLayer.prototype.setPane_ = function() {
if (!this.isAdded_) {
return;
}
// onAdd has been called, so panes can be used
var panes = this.getPanes();
if (!panes[this.paneName_]) {
throw new Error('"' + this.paneName_ + '" is not a valid MapPane name.');
}
panes[this.paneName_].appendChild(this.canvas);
};
/**
* Set a function that will be called whenever the parent map and the overlay's
* canvas have been resized. If opt_resizeHandler is null or unspecified, any
* existing callback is removed.
* @param {?function=} opt_resizeHandler The resize callback function.
*/
CanvasLayer.prototype.setResizeHandler = function(opt_resizeHandler) {
this.resizeHandler_ = opt_resizeHandler;
};
/**
* Set a function that will be called when a repaint of the canvas is required.
* If opt_updateHandler is null or unspecified, any existing callback is
* removed.
* @param {?function=} opt_updateHandler The update callback function.
*/
CanvasLayer.prototype.setUpdateHandler = function(opt_updateHandler) {
this.updateHandler_ = opt_updateHandler;
};
/**
* @inheritDoc
*/
CanvasLayer.prototype.onAdd = function() {
if (this.isAdded_) {
return;
}
this.isAdded_ = true;
this.setPane_();
this.resizeListener_ = google.maps.event.addListener(this.getMap(),
'resize', this.resizeFunction_);
this.centerListener_ = google.maps.event.addListener(this.getMap(),
'center_changed', this.repositionFunction_);
this.resize_();
this.repositionCanvas_();
this.readyHandler && this.readyHandler();
};
/**
* @inheritDoc
*/
CanvasLayer.prototype.onRemove = function() {
if (!this.isAdded_) {
return;
}
this.isAdded_ = false;
this.topLeft_ = null;
// remove canvas and listeners for pan and resize from map
this.canvas.parentElement.removeChild(this.canvas);
if (this.centerListener_) {
google.maps.event.removeListener(this.centerListener_);
this.centerListener_ = null;
}
if (this.resizeListener_) {
google.maps.event.removeListener(this.resizeListener_);
this.resizeListener_ = null;
}
// cease canvas update callbacks
if (this.requestAnimationFrameId_) {
this.cancelAnimFrame_.call(window, this.requestAnimationFrameId_);
this.requestAnimationFrameId_ = null;
}
};
/**
* The internal callback for resize events that resizes the canvas to keep the
* map properly covered.
* @private
*/
CanvasLayer.prototype.resize_ = function() {
// TODO(bckenny): it's common to use a smaller canvas but use CSS to scale
// what is drawn by the browser to save on fill rate. Add an option to do
// this.
if (!this.isAdded_) {
return;
}
var map = this.getMap();
var width = map.getDiv().offsetWidth;
var height = map.getDiv().offsetHeight;
var oldWidth = this.canvas.width;
var oldHeight = this.canvas.height;
// resizing may allocate a new back buffer, so do so conservatively
if (oldWidth !== width || oldHeight !== height) {
this.canvas.width = width;
this.canvas.height = height;
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
this.needsResize_ = true;
this.scheduleUpdate();
}
};
/**
* @inheritDoc
*/
CanvasLayer.prototype.draw = function() {
this.repositionCanvas_();
};
/**
* Internal callback for map view changes. Since the Maps API moves the overlay
* along with the map, this function calculates the opposite translation to
* keep the canvas in place.
* @private
*/
CanvasLayer.prototype.repositionCanvas_ = function() {
// TODO(bckenny): *should* only be executed on RAF, but in current browsers
// this causes noticeable hitches in map and overlay relative
// positioning.
var bounds = this.getMap().getBounds();
this.topLeft_ = new google.maps.LatLng(bounds.getNorthEast().lat(),
bounds.getSouthWest().lng());
// canvas position relative to draggable map's conatainer depends on
// overlayView's projection, not the map's
var projection = this.getProjection();
var divTopLeft = projection.fromLatLngToDivPixel(this.topLeft_);
// when the zoom level is low, more than one map can be shown in the screen
// so the canvas should be attach to the map with more are in the screen
var mapSize = (1 << this.getMap().getZoom())*256;
if (Math.abs(divTopLeft.x) > mapSize) {
divTopLeft.x -= mapSize;
}
this.canvas.style[CanvasLayer.CSS_TRANSFORM_] = 'translate(' +
Math.round(divTopLeft.x) + 'px,' + Math.round(divTopLeft.y) + 'px)';
this.scheduleUpdate();
};
/**
* Internal callback that serves as main animation scheduler via
* requestAnimationFrame. Calls resize and update callbacks if set, and
* schedules the next frame if overlay is animated.
* @private
*/
CanvasLayer.prototype.update_ = function() {
this.requestAnimationFrameId_ = null;
if (!this.isAdded_) {
return;
}
if (this.isAnimated_) {
this.scheduleUpdate();
}
if (this.needsResize_ && this.resizeHandler_) {
this.needsResize_ = false;
this.resizeHandler_();
}
if (this.updateHandler_) {
this.updateHandler_();
}
};
/**
* A convenience method to get the current LatLng coordinate of the top left of
* the current view of the map.
* @return {google.maps.LatLng} The top left coordinate.
*/
CanvasLayer.prototype.getTopLeft = function() {
return this.topLeft_;
};
/**
* Schedule a requestAnimationFrame callback to updateHandler. If one is
* already scheduled, there is no effect.
*/
CanvasLayer.prototype.scheduleUpdate = function() {
if (this.isAdded_ && !this.requestAnimationFrameId_) {
this.requestAnimationFrameId_ =
this.requestAnimFrame_.call(window, this.requestUpdateFunction_);
}
};
}
/*
====================
canvas setup for drawing tiles
====================
*/
if(typeof(google) !== 'undefined' && typeof(google.maps) !== 'undefined') {
function CanvasTileLayer(canvas_setup, render) {
this.tileSize = new google.maps.Size(256, 256);
this.maxZoom = 19;
this.name = "Tile #s";
this.alt = "Canvas tile layer";
this.tiles = {};
this.canvas_setup = canvas_setup;
this.render = render;
if (!render) {
this.render = canvas_setup;
}
}
// create a tile with a canvas element
CanvasTileLayer.prototype.create_tile_canvas = function (coord, zoom, ownerDocument) {
// create canvas and reset style
var canvas = ownerDocument.createElement('canvas');
var hit_canvas = ownerDocument.createElement('canvas');
canvas.style.border = hit_canvas.style.border = "none";
canvas.style.margin = hit_canvas.style.margin = "0";
canvas.style.padding = hit_canvas.style.padding = "0";
// prepare canvas and context sizes
var ctx = canvas.getContext('2d');
ctx.width = canvas.width = this.tileSize.width;
ctx.height = canvas.height = this.tileSize.height;
var hit_ctx = hit_canvas.getContext('2d');
hit_canvas.width = hit_ctx.width = this.tileSize.width;
hit_canvas.height = hit_ctx.height = this.tileSize.height;
//set unique id
var tile_id = coord.x + '_' + coord.y + '_' + zoom;
canvas.setAttribute('id', tile_id);
hit_canvas.setAttribute('id', tile_id);
if (tile_id in this.tiles)
delete this.tiles[tile_id];
this.tiles[tile_id] = {canvas:canvas, ctx:ctx, hit_canvas:hit_canvas, hit_ctx:hit_ctx, coord:coord, zoom:zoom, primitives:null};
// custom setup
//if (tile_id == '19295_24654_16'){
if (this.canvas_setup)
this.canvas_setup(this.tiles[tile_id], coord, zoom);
//}
return canvas;
}
CanvasTileLayer.prototype.each = function (callback) {
for (var t in this.tiles) {
var tile = this.tiles[t];
callback(tile);
}
}
CanvasTileLayer.prototype.recreate = function () {
for (var t in this.tiles) {
var tile = this.tiles[t];
this.canvas_setup(tile, tile.coord, tile.zoom);
}
};
CanvasTileLayer.prototype.redraw_tile = function (tile) {
this.render(tile, tile.coord, tile.zoom);
};
CanvasTileLayer.prototype.redraw = function () {
for (var t in this.tiles) {
var tile = this.tiles[t];
this.render(tile, tile.coord, tile.zoom);
}
};
// could be called directly...
CanvasTileLayer.prototype.getTile = function (coord, zoom, ownerDocument) {
return this.create_tile_canvas(coord, zoom, ownerDocument);
};
CanvasTileLayer.prototype.releaseTile = function (tile) {
var id = tile.getAttribute('id');
delete this.tiles[id];
};
}
(function(exports) {
if(typeof(google) === 'undefined' || typeof(google.maps) === 'undefined') return;
function GMapsTileLoader() {
}
GMapsTileLoader.prototype = {
_initTileLoader: function(map, projection) {
this._map = map;
this._projection = projection;
this._tiles = {};
this._tilesLoading = {};
this._tilesToLoad = 0;
this._updateTiles = this._updateTiles.bind(this);
this._listeners = [];
this._listeners.push(
google.maps.event.addListener(this._map, 'dragend', this._updateTiles),
google.maps.event.addListener(this._map, 'zoom_changed', this._updateTiles)
);
this.tileSize = 256;
this._updateTiles();
},
_removeTileLoader: function() {
for(var i in this._listeners) {
google.maps.event.removeListener(this._listeners[i]);
}
this._removeTiles();
},
_removeTiles: function () {
for (var key in this._tiles) {
this._removeTile(key);
}
},
_reloadTiles: function() {
this._removeTiles();
this._updateTiles();
},
_updateTiles: function () {
if (!this._map) { return; }
var bounds = this._map.getBounds();
var zoom = this._map.getZoom();
var tileSize = this.tileSize;
var mzoom = (1 << zoom);
var topLeft = new google.maps.LatLng(
bounds.getNorthEast().lat(),
bounds.getSouthWest().lng()
);
var bottomRigth = new google.maps.LatLng(
bounds.getSouthWest().lat(),
bounds.getNorthEast().lng()
);
this._projection = this._map.getProjection();
var divTopLeft = this._projection.fromLatLngToPoint(topLeft);
var divBottomRight = this._projection.fromLatLngToPoint(bottomRigth);
var nwTilePoint = new google.maps.Point(
Math.floor(divTopLeft.x*mzoom / tileSize),
Math.floor(divTopLeft.y*mzoom / tileSize)),
seTilePoint = new google.maps.Point(
Math.floor(divBottomRight.x*mzoom / tileSize),
Math.floor(divBottomRight.y*mzoom / tileSize));
this._addTilesFromCenterOut(nwTilePoint, seTilePoint);
this._removeOtherTiles(nwTilePoint, seTilePoint);
},
_removeOtherTiles: function (nwTilePoint, seTilePoint) {
var kArr, x, y, key;
var zoom = this._map.getZoom();
for (key in this._tiles) {
if (this._tiles.hasOwnProperty(key)) {
kArr = key.split(':');
x = parseInt(kArr[0], 10);
y = parseInt(kArr[1], 10);
z = parseInt(kArr[2], 10);
// remove tile if it's out of bounds
if (z !== zoom || x < nwTilePoint.x || x > seTilePoint.x || y < nwTilePoint.y || y > seTilePoint.y) {
this._removeTile(key);
}
}
}
},
_removeTile: function (key) {
this.onTileRemoved && this.onTileRemoved(this._tiles[key]);
delete this._tiles[key];
delete this._tilesLoading[key];
},
_tileKey: function(tilePoint) {
return tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom;
},
_tileShouldBeLoaded: function (tilePoint) {
var k = this._tileKey(tilePoint);
return !(k in this._tiles) && !(k in this._tilesLoading);
},
_tileLoaded: function(tilePoint, tileData) {
this._tilesToLoad--;
var k = tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom
this._tiles[k] = tileData;
delete this._tilesLoading[k];
if(this._tilesToLoad === 0) {
this.onTilesLoaded && this.onTilesLoaded();
}
},
getTilePos: function (tilePoint) {
var limit = (1 << this._map.getZoom());
// wrap tile
tilePoint = {
x: ((tilePoint.x % limit) + limit) % limit,
y: tilePoint.y
};
tilePoint = new google.maps.Point(
tilePoint.x * this.tileSize,
tilePoint.y * this.tileSize
);
var bounds = this._map.getBounds();
var topLeft = new google.maps.LatLng(
bounds.getNorthEast().lat(),
bounds.getSouthWest().lng()
);
var divTopLeft = this._map.getProjection().fromLatLngToPoint(topLeft);
zoom = (1 << this._map.getZoom());
divTopLeft.x = divTopLeft.x * zoom;
divTopLeft.y = divTopLeft.y * zoom;
return new google.maps.Point(
tilePoint.x - divTopLeft.x,
tilePoint.y - divTopLeft.y
);
},
_addTilesFromCenterOut: function (nwTilePoint, seTilePoint) {
var queue = [],
center = new google.maps.Point(
(nwTilePoint.x + seTilePoint.x) * 0.5,
(nwTilePoint.y + seTilePoint.y) * 0.5
),
zoom = this._map.getZoom();
var j, i, point;
for (j = nwTilePoint.y; j <= seTilePoint.y; j++) {
for (i = nwTilePoint.x; i <= seTilePoint.x; i++) {
point = new google.maps.Point (i, j);
point.zoom = zoom;
if (this._tileShouldBeLoaded(point)) {
queue.push(point);
}
}
}
var tilesToLoad = queue.length;
if (tilesToLoad === 0) { return; }
function distanceToCenterSq(point) {
var dx = point.x - center.x;
var dy = point.y - center.y;
return dx * dx + dy * dy;
}
// load tiles in order of their distance to center
queue.sort(function (a, b) {
return distanceToCenterSq(a) - distanceToCenterSq(b);
});
this._tilesToLoad += tilesToLoad;
for (i = 0; i < tilesToLoad; i++) {
var t = queue[i];
var k = this._tileKey(t);
this._tilesLoading[k] = t;
// events
if (this.onTileAdded) {
this.onTileAdded(t);
}
}
this.onTilesLoading && this.onTilesLoading();
}
}
torque.GMapsTileLoader = GMapsTileLoader;
})(typeof exports === "undefined" ? this : exports);
(function(exports) {
if(typeof(google) === 'undefined' || typeof(google.maps) === 'undefined') return;
function GMapsTorqueLayer(options) {
var self = this;
if (!torque.isBrowserSupported()) {
throw new Error("browser is not supported by torque");
}
this.key = 0;
this.shader = null;
this.ready = false;
this.options = _.extend({}, options);
_.defaults(this.options, {
provider: 'windshaft',
renderer: 'point',
resolution: 2,
steps: 100,
visible: true
});
if (options.cartocss) {
_.extend(this.options,
torque.common.TorqueLayer.optionsFromCartoCSS(options.cartocss));
}
this.hidden = !this.options.visible;
this.animator = new torque.Animator(function(time) {
var k = time | 0;
if(self.key !== k) {
self.setKey(k);
}
}, this.options);
this.play = this.animator.start.bind(this.animator);
this.stop = this.animator.stop.bind(this.animator);
this.pause = this.animator.pause.bind(this.animator);
this.toggle = this.animator.toggle.bind(this.animator);
this.setDuration = this.animator.duration.bind(this.animator);
this.isRunning = this.animator.isRunning.bind(this.animator);
CanvasLayer.call(this, {
map: this.options.map,
//resizeHandler: this.redraw,
animate: false,
updateHandler: this.render,
readyHandler: this.initialize
});
}
/**
* torque layer
*/
GMapsTorqueLayer.prototype = _.extend({},
CanvasLayer.prototype,
torque.GMapsTileLoader.prototype,
torque.Event,
{
providers: {
'sql_api': torque.providers.json,
'url_template': torque.providers.jsonarray,
'windshaft': torque.providers.windshaft
},
renderers: {
'point': torque.renderer.Point,
'pixel': torque.renderer.Rectangle
},
initialize: function() {
var self = this;
this.onTileAdded = this.onTileAdded.bind(this);
this.options.ready = function() {
self.fire("change:bounds", {
bounds: self.provider.getBounds()
});
self.animator.steps(self.provider.getSteps());
self.animator.rescale();
self.fire('change:steps', {
steps: self.provider.getSteps()
});
self.setKey(self.key);
};
this.provider = new this.providers[this.options.provider](this.options);
this.renderer = new this.renderers[this.options.renderer](this.getCanvas(), this.options);
// this listener should be before tile loader
this._cacheListener = google.maps.event.addListener(this.options.map, 'zoom_changed', function() {
self.renderer && self.renderer.clearSpriteCache();
});
this._initTileLoader(this.options.map, this.getProjection());
if (this.shader) {
this.renderer.setShader(this.shader);
}
},
hide: function() {
if(this.hidden) return this;
this.pause();
this.clear();
this.hidden = true;
return this;
},
show: function() {
if(!this.hidden) return this;
this.hidden = false;
this.play();
return this;
},
setSQL: function(sql) {
if (!this.provider || !this.provider.setSQL) {
throw new Error("this provider does not support SQL");
}
this.provider.setSQL(sql);
this._reloadTiles();
return this;
},
setBlendMode: function(_) {
this.renderer && this.renderer.setBlendMode(_);
this.redraw();
},
setSteps: function(steps) {
this.provider && this.provider.setSteps(steps);
this.animator && this.animator.steps(steps);
this._reloadTiles();
},
setColumn: function(column, isTime) {
this.provider && this.provider.setColumn(column, isTime);
this._reloadTiles();
},
getTimeBounds: function() {
return this.provider && this.provider.getKeySpan();
},
getCanvas: function() {
return this.canvas;
},
// for each tile shown on the map request the data
onTileAdded: function(t) {
var self = this;
this.provider.getTileData(t, t.zoom, function(tileData) {
// don't load tiles that are not being shown
if (t.zoom !== self.options.map.getZoom()) return;
self._tileLoaded(t, tileData);
if (tileData) {
self.redraw();
}
});
},
clear: function() {
var canvas = this.canvas;
canvas.width = canvas.width;
},
/**
* render the selectef key
* don't call this function directly, it's called by
* requestAnimationFrame. Use redraw to refresh it
*/
render: function() {
if(this.hidden) return;
var t, tile, pos;
var canvas = this.canvas;
canvas.width = canvas.width;
var ctx = canvas.getContext('2d');
// renders only a "frame"
for(t in this._tiles) {
tile = this._tiles[t];
if (tile) {
pos = this.getTilePos(tile.coord);
ctx.setTransform(1, 0, 0, 1, pos.x, pos.y);
this.renderer.renderTile(tile, this.key, pos.x, pos.y);
}
}
},
/**
* set key to be shown. If it's a single value
* it renders directly, if it's an array it renders
* accumulated
*/
setKey: function(key) {
this.key = key;
this.animator.step(key);
this.redraw();
this.fire('change:time', { time: this.getTime(), step: this.key });
},
/**
* helper function, does the same than ``setKey`` but only
* accepts scalars.
*/
setStep: function(time) {
if(time === undefined || time.length !== undefined) {
throw new Error("setTime only accept scalars");
}
this.setKey(time);
},
/**
* transform from animation step to Date object
* that contains the animation time
*
* ``step`` should be between 0 and ``steps - 1``
*/
stepToTime: function(step) {
if (!this.provider) return 0;
var times = this.provider.getKeySpan();
var time = times.start + (times.end - times.start)*(step/this.provider.getSteps());
return new Date(time);
},
getStep: function() {
return this.key;
},
/**
* returns the animation time defined by the data
* in the defined column. Date object
*/
getTime: function() {
return this.stepToTime(this.key);
},
/**
* set the cartocss for the current renderer
*/
setCartoCSS: function(cartocss) {
var shader = new carto.RendererJS().render(cartocss);
this.shader = shader;
if (this.renderer) {
this.renderer.setShader(shader);
}
// provider options
var options = torque.common.TorqueLayer.optionsFromLayer(shader.findLayer({ name: 'Map' }));
this.provider && this.provider.setCartoCSS && this.provider.setCartoCSS(cartocss);
if(this.provider && this.provider.setOptions(options)) {
this._reloadTiles();
}
_.extend(this.options, options);
// animator options
if (options.animationDuration) {
this.animator.duration(options.animationDuration);
}
this.redraw();
return this;
},
redraw: function() {
this.scheduleUpdate();
},
onRemove: function() {
CanvasLayer.prototype.onRemove.call(this);
this.animator.stop();
this._removeTileLoader();
google.maps.event.removeListener(this._cacheListener);
}
});
function GMapsTiledTorqueLayer(options) {
this.options = _.extend({}, options);
CanvasTileLayer.call(this, this._loadTile.bind(this), this.drawTile.bind(this));
this.initialize(options);
}
GMapsTiledTorqueLayer.prototype = _.extend({}, CanvasTileLayer.prototype, {
providers: {
'sql_api': torque.providers.json,
'url_template': torque.providers.JsonArray
},
renderers: {
'point': torque.renderer.Point,
'pixel': torque.renderer.Rectangle
},
initialize: function(options) {
var self = this;
this.key = 0;
this.options.renderer = this.options.renderer || 'pixel';
this.options.provider = this.options.provider || 'sql_api';
this.provider = new this.providers[this.options.provider](options);
this.renderer = new this.renderers[this.options.renderer](null, options);
},
_tileLoaded: function(tile, tileData) {
tile.data = tileData;
this.drawTile(tile);
},
_loadTile: function(tile, coord, zoom) {
var self = this;
var limit = 1 << zoom;
// wrap tile
var wrappedCoord = {
x: ((coord.x % limit) + limit) % limit,
y: coord.y
};
this.provider.getTileData(wrappedCoord, zoom, function(tileData) {
self._tileLoaded(tile, tileData);
});
},
drawTile: function (tile) {
var canvas = tile.canvas;
if(!tile.data) return;
canvas.width = canvas.width;
this.renderer.setCanvas(canvas);
var accum = this.renderer.accumulate(tile.data, this.key);
this.renderer.renderTileAccum(accum, 0, 0);
},
setKey: function(key) {
this.key = key;
this.redraw();
},
/**
* set the cartocss for the current renderer
*/
setCartoCSS: function(cartocss) {
if (!this.renderer) throw new Error('renderer is not valid');
return this.renderer.setCartoCSS(cartocss);
}
});
exports.torque.GMapsTiledTorqueLayer = GMapsTiledTorqueLayer;
exports.torque.GMapsTorqueLayer = GMapsTorqueLayer;
})(typeof exports === "undefined" ? this : exports);
if(typeof(L) !== 'undefined') {
L.Mixin.TileLoader = {
_initTileLoader: function() {
this._tiles = {}
this._tilesLoading = {};
this._tilesToLoad = 0;
this._map.on({
'moveend': this._updateTiles
}, this);
this._updateTiles();
},
_removeTileLoader: function() {
this._map.off({
'moveend': this._updateTiles
}, this);
this._removeTiles();
},
_updateTiles: function () {
if (!this._map) { return; }
var bounds = this._map.getPixelBounds(),
zoom = this._map.getZoom(),
tileSize = this.options.tileSize;
if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {
return;
}
var nwTilePoint = new L.Point(
Math.floor(bounds.min.x / tileSize),
Math.floor(bounds.min.y / tileSize)),
seTilePoint = new L.Point(
Math.floor(bounds.max.x / tileSize),
Math.floor(bounds.max.y / tileSize)),
tileBounds = new L.Bounds(nwTilePoint, seTilePoint);
this._addTilesFromCenterOut(tileBounds);
this._removeOtherTiles(tileBounds);
},
_removeTiles: function (bounds) {
for (var key in this._tiles) {
this._removeTile(key);
}
},
_reloadTiles: function() {
this._removeTiles();
this._updateTiles();
},
_removeOtherTiles: function (bounds) {
var kArr, x, y, z, key;
var zoom = this._map.getZoom();
for (key in this._tiles) {
if (this._tiles.hasOwnProperty(key)) {
kArr = key.split(':');
x = parseInt(kArr[0], 10);
y = parseInt(kArr[1], 10);
z = parseInt(kArr[2], 10);
// remove tile if it's out of bounds
if (zoom !== z || x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) {
this._removeTile(key);
}
}
}
},
_removeTile: function (key) {
this.fire('tileRemoved', this._tiles[key]);
delete this._tiles[key];
delete this._tilesLoading[key];
},
_tileKey: function(tilePoint) {
return tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom;
},
_tileShouldBeLoaded: function (tilePoint) {
var k = this._tileKey(tilePoint);
return !(k in this._tiles) && !(k in this._tilesLoading);
},
_tileLoaded: function(tilePoint, tileData) {
this._tilesToLoad--;
var k = tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom
this._tiles[k] = tileData;
delete this._tilesLoading[k];
if(this._tilesToLoad === 0) {
this.fire("tilesLoaded");
}
},
getTilePos: function (tilePoint) {
tilePoint = new L.Point(tilePoint.x, tilePoint.y);
var origin = this._map._getNewTopLeftPoint(this._map.getCenter()),
tileSize = this.options.tileSize;
return tilePoint.multiplyBy(tileSize).subtract(origin);
},
_addTilesFromCenterOut: function (bounds) {
var queue = [],
center = bounds.getCenter(),
zoom = this._map.getZoom();
var j, i, point;
for (j = bounds.min.y; j <= bounds.max.y; j++) {
for (i = bounds.min.x; i <= bounds.max.x; i++) {
point = new L.Point(i, j);
point.zoom = zoom;
if (this._tileShouldBeLoaded(point)) {
queue.push(point);
}
}
}
var tilesToLoad = queue.length;
if (tilesToLoad === 0) { return; }
// load tiles in order of their distance to center
queue.sort(function (a, b) {
return a.distanceTo(center) - b.distanceTo(center);
});
this._tilesToLoad += tilesToLoad;
for (i = 0; i < tilesToLoad; i++) {
var t = queue[i];
var k = this._tileKey(t);
this._tilesLoading[k] = t;
this.fire('tileAdded', t);
}
this.fire("tilesLoading");
}
}
} //L defined
if(typeof(L) !== 'undefined') {
/**
* full canvas layer implementation for Leaflet
*/
L.CanvasLayer = L.Class.extend({
includes: [L.Mixin.Events, L.Mixin.TileLoader],
options: {
minZoom: 0,
maxZoom: 28,
tileSize: 256,
subdomains: 'abc',
errorTileUrl: '',
attribution: '',
zoomOffset: 0,
opacity: 1,
unloadInvisibleTiles: L.Browser.mobile,
updateWhenIdle: L.Browser.mobile,
tileLoader: false, // installs tile loading events
zoomAnimation: true
},
initialize: function (options) {
var self = this;
options = options || {};
//this.project = this._project.bind(this);
this.render = this.render.bind(this);
L.Util.setOptions(this, options);
this._canvas = this._createCanvas();
// backCanvas for zoom animation
if (this.options.zoomAnimation) {
this._backCanvas = this._createCanvas();
}
this._ctx = this._canvas.getContext('2d');
this.currentAnimationFrame = -1;
this.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
return window.setTimeout(callback, 1000 / 60);
};
this.cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame ||
window.webkitCancelAnimationFrame || window.msCancelAnimationFrame || function(id) { clearTimeout(id); };
},
_createCanvas: function() {
var canvas;
canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.pointerEvents = "none";
canvas.style.zIndex = this.options.zIndex || 0;
var className = 'leaflet-tile-container';
if (this.options.zoomAnimation) {
className += ' leaflet-zoom-animated';
}
canvas.setAttribute('class', className);
return canvas;
},
onAdd: function (map) {
this._map = map;
// add container with the canvas to the tile pane
// the container is moved in the oposite direction of the
// map pane to keep the canvas always in (0, 0)
var tilePane = this._map._panes.tilePane;
var _container = L.DomUtil.create('div', 'leaflet-layer');
_container.appendChild(this._canvas);
if (this.options.zoomAnimation) {
_container.appendChild(this._backCanvas);
this._backCanvas.style.display = 'none';
}
tilePane.appendChild(_container);
this._container = _container;
// hack: listen to predrag event launched by dragging to
// set container in position (0, 0) in screen coordinates
map.dragging._draggable.on('predrag', function() {
var d = map.dragging._draggable;
L.DomUtil.setPosition(this._canvas, { x: -d._newPos.x, y: -d._newPos.y });
}, this);
map.on({ 'viewreset': this._reset }, this);
map.on('move', this.render, this);
map.on('resize', this._reset, this);
if (this.options.zoomAnimation) {
map.on({
'zoomanim': this._animateZoom,
'zoomend': this._endZoomAnim
}, this);
}
if(this.options.tileLoader) {
this._initTileLoader();
}
this._reset();
},
_animateZoom: function (e) {
if (!this._animating) {
this._animating = true;
}
var back = this._backCanvas;
back.width = this._canvas.width;
back.height = this._canvas.height;
// paint current canvas in back canvas with trasnformation
var pos = this._canvas._leaflet_pos || { x: 0, y: 0 };
back.getContext('2d').drawImage(this._canvas, 0, 0);
// hide original
this._canvas.style.display = 'none';
back.style.display = 'block';
var map = this._map;
var scale = map.getZoomScale(e.zoom);
var newCenter = map._latLngToNewLayerPoint(map.getCenter(), e.zoom, e.center);
var oldCenter = map._latLngToNewLayerPoint(e.center, e.zoom, e.center);
var origin = {
x: newCenter.x - oldCenter.x + pos.x,
y: newCenter.y - oldCenter.y + pos.y,
};
var bg = back;
var transform = L.DomUtil.TRANSFORM;
bg.style[transform] = L.DomUtil.getTranslateString(origin) + ' scale(' + e.scale + ') ';
},
_endZoomAnim: function () {
this._animating = false;
this._canvas.style.display = 'block';
this._backCanvas.style.display = 'none';
},
getCanvas: function() {
return this._canvas;
},
getAttribution: function() {
return this.options.attribution;
},
draw: function() {
return this._reset();
},
onRemove: function (map) {
this._container.parentNode.removeChild(this._container);
map.off({
'viewreset': this._reset,
'move': this._render,
'resize': this._reset,
'zoomanim': this._animateZoom,
'zoomend': this._endZoomAnim
}, this);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
this._updateOpacity();
return this;
},
setZIndex: function(zIndex) {
this._canvas.style.zIndex = zIndex;
if (this.options.zoomAnimation) {
this._backCanvas.style.zIndex = zIndex;
}
},
bringToFront: function () {
return this;
},
bringToBack: function () {
return this;
},
_reset: function () {
var size = this._map.getSize();
this._canvas.width = size.x;
this._canvas.height = size.y;
// fix position
var pos = L.DomUtil.getPosition(this._map.getPanes().mapPane);
if (pos) {
L.DomUtil.setPosition(this._canvas, { x: -pos.x, y: -pos.y });
}
this.onResize();
this._render();
},
/*
_project: function(x) {
var point = this._map.latLngToLayerPoint(new L.LatLng(x[1], x[0]));
return [point.x, point.y];
},
*/
_updateOpacity: function () { },
_render: function() {
if (this.currentAnimationFrame >= 0) {
this.cancelAnimationFrame.call(window, this.currentAnimationFrame);
}
this.currentAnimationFrame = this.requestAnimationFrame.call(window, this.render);
},
// use direct: true if you are inside an animation frame call
redraw: function(direct) {
if (direct) {
this.render();
} else {
this._render();
}
},
onResize: function() {
},
render: function() {
throw new Error('render function should be implemented');
}
});
} //L defined
if(typeof(L) !== 'undefined') {
/**
* torque layer
*/
L.TorqueLayer = L.CanvasLayer.extend({
providers: {
'sql_api': torque.providers.json,
'url_template': torque.providers.jsonarray,
'windshaft': torque.providers.windshaft
},
renderers: {
'point': torque.renderer.Point,
'pixel': torque.renderer.Rectangle
},
initialize: function(options) {
var self = this;
if (!torque.isBrowserSupported()) {
throw new Error("browser is not supported by torque");
}
options.tileLoader = true;
this.key = 0;
if (options.cartocss) {
_.extend(options, torque.common.TorqueLayer.optionsFromCartoCSS(options.cartocss));
}
options.resolution = options.resolution || 2;
options.steps = options.steps || 100;
options.visible = options.visible === undefined ? true: options.visible;
this.hidden = !options.visible;
this.animator = new torque.Animator(function(time) {
var k = time | 0;
if(self.key !== k) {
self.setKey(k, { direct: true });
}
}, options);
this.play = this.animator.start.bind(this.animator);
this.stop = this.animator.stop.bind(this.animator);
this.pause = this.animator.pause.bind(this.animator);
this.toggle = this.animator.toggle.bind(this.animator);
this.setDuration = this.animator.duration.bind(this.animator);
this.isRunning = this.animator.isRunning.bind(this.animator);
L.CanvasLayer.prototype.initialize.call(this, options);
this.options.renderer = this.options.renderer || 'point';
this.options.provider = this.options.provider || 'windshaft';
options.ready = function() {
self.fire("change:bounds", {
bounds: self.provider.getBounds()
});
self.animator.steps(self.provider.getSteps());
self.animator.rescale();
self.fire('change:steps', {
steps: self.provider.getSteps()
});
self.setKey(self.key);
};
this.provider = new this.providers[this.options.provider](options);
this.renderer = new this.renderers[this.options.renderer](this.getCanvas(), options);
// for each tile shown on the map request the data
this.on('tileAdded', function(t) {
var tileData = this.provider.getTileData(t, t.zoom, function(tileData) {
// don't load tiles that are not being shown
if (t.zoom !== self._map.getZoom()) return;
self._tileLoaded(t, tileData);
if (tileData) {
self.redraw();
}
});
}, this);
},
_clearCaches: function() {
this.renderer && this.renderer.clearSpriteCache();
},
onAdd: function (map) {
map.on({
'zoomend': this._clearCaches,
'zoomstart': this._pauseOnZoom,
}, this);
map.on({
'zoomend': this._resumeOnZoom
}, this);
L.CanvasLayer.prototype.onAdd.call(this, map);
},
onRemove: function(map) {
this._removeTileLoader();
map.off({
'zoomend': this._clearCaches,
'zoomstart': this._pauseOnZoom,
}, this);
map.off({
'zoomend': this._resumeOnZoom
}, this);
L.CanvasLayer.prototype.onRemove.call(this, map);
},
_pauseOnZoom: function() {
this.wasRunning = this.isRunning();
if (this.wasRunning) {
this.pause();
}
},
_resumeOnZoom: function() {
if (this.wasRunning) {
this.play();
}
},
hide: function() {
if(this.hidden) return this;
this.pause();
this.clear();
this.hidden = true;
return this;
},
show: function() {
if(!this.hidden) return this;
this.hidden = false;
this.play();
return this;
},
setSQL: function(sql) {
if (!this.provider || !this.provider.setSQL) {
throw new Error("this provider does not support SQL");
}
this.provider.setSQL(sql);
this._reloadTiles();
return this;
},
setBlendMode: function(_) {
this.renderer.setBlendMode(_);
this.redraw();
},
setSteps: function(steps) {
this.provider.setSteps(steps);
this._reloadTiles();
},
setColumn: function(column, isTime) {
this.provider.setColumn(column, isTime);
this._reloadTiles();
},
getTimeBounds: function() {
return this.provider && this.provider.getKeySpan();
},
clear: function() {
var canvas = this.getCanvas();
canvas.width = canvas.width;
},
/**
* render the selectef key
* don't call this function directly, it's called by
* requestAnimationFrame. Use redraw to refresh it
*/
render: function() {
if(this.hidden) return;
var t, tile, pos;
var canvas = this.getCanvas();
canvas.width = canvas.width;
var ctx = canvas.getContext('2d');
for(t in this._tiles) {
tile = this._tiles[t];
if (tile) {
pos = this.getTilePos(tile.coord);
ctx.setTransform(1, 0, 0, 1, pos.x, pos.y);
this.renderer.renderTile(tile, this.key, pos.x, pos.y);
}
}
},
/**
* set key to be shown. If it's a single value
* it renders directly, if it's an array it renders
* accumulated
*/
setKey: function(key, options) {
this.key = key;
this.animator.step(key);
this.redraw(options && options.direct);
this.fire('change:time', { time: this.getTime(), step: this.key });
},
/**
* helper function, does the same than ``setKey`` but only
* accepts scalars.
*/
setStep: function(time) {
if(time === undefined || time.length !== undefined) {
throw new Error("setTime only accept scalars");
}
this.setKey(time);
},
/**
* transform from animation step to Date object
* that contains the animation time
*
* ``step`` should be between 0 and ``steps - 1``
*/
stepToTime: function(step) {
var times = this.provider.getKeySpan();
var time = times.start + (times.end - times.start)*(step/this.provider.getSteps());
return new Date(time);
},
getStep: function() {
return this.key;
},
/**
* returns the animation time defined by the data
* in the defined column. Date object
*/
getTime: function() {
return this.stepToTime(this.key);
},
/**
* returns an object with the start and end times
*/
getTimeSpan: function() {
var times = this.provider.getKeySpan();
},
/**
* set the cartocss for the current renderer
*/
setCartoCSS: function(cartocss) {
if (!this.renderer) throw new Error('renderer is not valid');
var shader = new carto.RendererJS().render(cartocss);
this.renderer.setShader(shader);
// provider options
var options = torque.common.TorqueLayer.optionsFromLayer(shader.findLayer({ name: 'Map' }));
this.provider.setCartoCSS && this.provider.setCartoCSS(cartocss);
if(this.provider.setOptions(options)) {
this._reloadTiles();
}
_.extend(this.options, options);
// animator options
if (options.animationDuration) {
this.animator.duration(options.animationDuration);
}
this.redraw();
return this;
}
});
} //L defined