4433 lines
124 KiB
JavaScript
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
|