2264 lines
62 KiB
JavaScript
2264 lines
62 KiB
JavaScript
(function(exports) {
|
|
|
|
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
|
|
|
|
var cancelAnimationFrame = window.requestAnimationFrame || window.mozCancelAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
|
|
|
|
/**
|
|
* 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._animFrame = null;
|
|
this._time = 0.0;
|
|
|
|
this.domain = invLinear(this.options.animationDelay, this.options.animationDelay, this.options.animationDuration);
|
|
this.range = linear(0, this.options.steps);
|
|
}
|
|
|
|
|
|
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);
|
|
return function(t) {
|
|
return c(a*(1.0 - t) + t*b);
|
|
};
|
|
}
|
|
|
|
|
|
Animator.prototype = {
|
|
|
|
start: function() {
|
|
this.running = true;
|
|
this._animFrame = requestAnimationFrame(this._tick);
|
|
},
|
|
|
|
stop: function() {
|
|
this.running = true;
|
|
cancelAnimationFrame(this._animFrame);
|
|
},
|
|
|
|
_tick: function() {
|
|
var t1 = +new Date();
|
|
var delta = (t1 - this._t0)*0.001;
|
|
this._t0 = t1;
|
|
this._time += delta;
|
|
this.callback(this.range(this.domain(this._time)));
|
|
this._animFrame = requestAnimationFrame(this._tick);
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
})(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" //
|
|
]
|
|
}
|
|
},
|
|
"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"
|
|
}
|
|
},
|
|
"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"
|
|
}
|
|
},
|
|
"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);
|
|
/*
|
|
# 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: 10000000,
|
|
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();
|
|
},
|
|
|
|
// 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) {
|
|
|
|
exports.torque = exports.torque || {};
|
|
var providers = exports.torque.providers = exports.torque.providers || {};
|
|
|
|
// 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;
|
|
|
|
// 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.getKeySpan();
|
|
} else {
|
|
this._ready = 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 x = new Uint8Array(rows.length);
|
|
var y = new Uint8Array(rows.length);
|
|
|
|
// count number of dates
|
|
var dates = 0;
|
|
var maxDateSlots = 0;
|
|
for (var r = 0; r < rows.length; ++r) {
|
|
var row = rows[r];
|
|
dates += row['dates__uint16'].length;
|
|
maxDateSlots = Math.max(maxDateSlots, row.dates__uint16.length);
|
|
}
|
|
|
|
// reserve memory for all the dates
|
|
var timeIndex = new Int32Array(maxDateSlots); //index-size
|
|
var timeCount = new Int32Array(maxDateSlots);
|
|
var renderData = new 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__uint8;
|
|
y[r] = row.y__uint8;
|
|
|
|
var dates = rows[r]['dates__uint16'];
|
|
var vals = rows[r]['vals__uint8'];
|
|
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;
|
|
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;
|
|
}
|
|
}
|
|
/*
|
|
for (var r = 0; r < rows.length; ++r) {
|
|
var dates = rows.get('dates__uint16')[r];
|
|
var vals = rows.get('vals__uint8')[r];
|
|
for (var j = 0, len = dates.length; j < len; ++j) {
|
|
if(dates[j] == i) {
|
|
++c;
|
|
renderData[renderDataIndex] = vals[j];
|
|
renderDataPos[renderDataIndex] = r;
|
|
++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 || 'http://' + this.options.user + '.cartodb.com/api/v2/sql';
|
|
},
|
|
|
|
// execute actual query
|
|
sql: function(sql, callback, options) {
|
|
options = options || {};
|
|
torque.net.get(this.url() + "?q=" + encodeURIComponent(sql), function (data) {
|
|
if(options.parseJSON) {
|
|
data = JSON.parse(data.responseText);
|
|
}
|
|
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();
|
|
},
|
|
|
|
_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) {
|
|
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" +
|
|
", 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 {table} i, par p " +
|
|
" WHERE i.the_geom_webmercator && p.ext " +
|
|
" GROUP BY g, d" +
|
|
") " +
|
|
"" +
|
|
"SELECT least((st_x(g)-st_xmin(p.ext))/p.res, 255) x__uint8, " +
|
|
" least((st_y(g)-st_ymin(p.ext))/p.res, 255) y__uint8," +
|
|
" array_agg(c) vals__uint8," +
|
|
" array_agg(d) dates__uint16" +
|
|
" FROM cte, par p GROUP BY x__uint8, y__uint8";
|
|
|
|
var query = format(sql, this.options, {
|
|
zoom: zoom,
|
|
x: coord.x,
|
|
y: coord.y,
|
|
column_conv: column_conv
|
|
});
|
|
|
|
var self = this;
|
|
this.sql(query, function (data) {
|
|
var rows = JSON.parse(data.responseText).rows;
|
|
callback(self.proccessTile(rows, coord, zoom));
|
|
});
|
|
},
|
|
|
|
//
|
|
// 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.
|
|
//
|
|
getKeySpan: function() {
|
|
var max_col, min_col, max_tmpl, min_tmpl;
|
|
|
|
if (this.options.is_time){
|
|
max_tmpl = "date_part('epoch', max({column}))";
|
|
min_tmpl = "date_part('epoch', min({column}))";
|
|
} else {
|
|
max_tmpl = "max({0})";
|
|
min_tmpl = "min({0})";
|
|
}
|
|
|
|
max_col = format(max_tmpl, { column: this.options.column });
|
|
min_col = format(min_tmpl, { column: this.options.column });
|
|
|
|
var sql = format("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 {table}", {
|
|
max_col: max_col,
|
|
min_col: min_col,
|
|
table: this.options.table
|
|
})
|
|
|
|
var self = this;
|
|
this.sql(sql, function(data) {
|
|
//TODO: manage bounds
|
|
data = data.rows[0];
|
|
self.options.start = data.min;
|
|
self.options.step = (data.max - data.min)/self.options.steps;
|
|
self._setReady(true);
|
|
}, { parseJSON: true });
|
|
}
|
|
|
|
};
|
|
|
|
torque.providers.json = json
|
|
|
|
|
|
})(typeof exports === "undefined" ? this : exports);
|
|
(function(exports) {
|
|
|
|
exports.torque = exports.torque || {};
|
|
var providers = exports.torque.providers = exports.torque.providers || {};
|
|
|
|
// 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.cummulative) {
|
|
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 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;
|
|
},
|
|
|
|
|
|
getTile: function(coord, zoom, callback) {
|
|
var template = this.url();
|
|
template = template
|
|
.replace('{x}', coord.x)
|
|
.replace('{y}', coord.y)
|
|
.replace('{z}', 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.url();
|
|
template = template
|
|
.replace('{x}', coord.x)
|
|
.replace('{y}', coord.y)
|
|
.replace('{z}', 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');
|
|
try {
|
|
processingTime.start();
|
|
var rows = JSON.parse(data.responseText).rows;
|
|
processed = self.proccessTile(rows, coord, zoom);
|
|
processingTime.end();
|
|
} catch(e) {
|
|
processingTime.end();
|
|
console.error("problem parsing JSON on ", coord, zoom);
|
|
}
|
|
|
|
if(processed) {
|
|
callback(processed);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
torque.providers.JsonArray = json
|
|
|
|
|
|
})(typeof exports === "undefined" ? this : exports);
|
|
(function(exports) {
|
|
var torque = exports.torque = exports.torque || {};
|
|
torque.net = torque.net || {};
|
|
|
|
function get(url, callback) {
|
|
var req = new XMLHttpRequest();
|
|
req.onreadystatechange = function() {
|
|
if (req.readyState == 4){
|
|
if (req.status == 200){
|
|
callback(req);
|
|
} else {
|
|
callback(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
req.open("GET", url, true);
|
|
//req.responseType = 'arraybuffer';
|
|
req.send(null)
|
|
return req;
|
|
}
|
|
|
|
torque.net = {
|
|
get: get
|
|
};
|
|
|
|
})(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
|
|
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();
|
|
}
|
|
ctx.globalAlpha = 1.0;
|
|
if(st.strokeStyle) {
|
|
if(st.strokeOpacity) {
|
|
ctx.globalAlpha = st.strokeOpacity;
|
|
}
|
|
if(st.lineWidth) {
|
|
ctx.lineWidth = st.lineWidth;
|
|
}
|
|
ctx.strokeStyle = st.strokeStyle;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
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
|
|
};
|
|
|
|
})(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: 20;',
|
|
' [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 = {};
|
|
//carto.tree.Reference.set(torque['torque-reference']);
|
|
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._sprites = {};
|
|
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];
|
|
},
|
|
|
|
//
|
|
// generate sprite based on cartocss style
|
|
//
|
|
generateSprite: function(value, tile) {
|
|
var st = this._shader.getStyle('canvas-2d', {
|
|
value: value
|
|
}, { zoom: tile.zoom });
|
|
|
|
var pointSize = st['point-radius'];
|
|
if(!pointSize) {
|
|
throw new Error("marker-width property should be set");
|
|
}
|
|
var canvasSize = pointSize*2;
|
|
|
|
var canvas = document.createElement('canvas');
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.width = canvas.width = canvasSize;
|
|
ctx.height = canvas.height = canvasSize;
|
|
ctx.translate(pointSize, pointSize);
|
|
if(st['point-file'] || st['marker-fil']) {
|
|
torque.cartocss.renderSprite(ctx, st);
|
|
} else {
|
|
torque.cartocss.renderPoint(ctx, st);
|
|
}
|
|
return canvas;
|
|
},
|
|
|
|
//
|
|
// renders a tile in the canvas for key defined in
|
|
// the torque tile
|
|
//
|
|
renderTile: function(tile, key) {
|
|
if(!this._canvas) return;
|
|
//var prof = Profiler.get('render').start();
|
|
var ctx = this._ctx;
|
|
var res = this.options.resolution;
|
|
var sprites = this._sprites;
|
|
var activePixels = tile.timeCount[key];
|
|
if(this.options.blendmode) {
|
|
ctx.globalCompositeOperation = this.options.blendmode;
|
|
}
|
|
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(c, tile);
|
|
}
|
|
var x = tile.x[posIdx] - (sp.width >> 1);
|
|
var y = tile.y[posIdx] - (sp.height >> 1);
|
|
ctx.drawImage(sp, x*res, 255 - y*res);
|
|
}
|
|
}
|
|
}
|
|
//prof.end();
|
|
}
|
|
};
|
|
|
|
|
|
// 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 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];
|
|
}
|
|
}
|
|
}
|
|
return accum;
|
|
},
|
|
|
|
renderTileAccum: function(accum, px, py) {
|
|
var color, x, y;
|
|
var res = this.options.resolution;
|
|
var ctx = this._ctx;
|
|
var s = (256/res) | 0;
|
|
var s2 = s*s;
|
|
var colors = this._colors;
|
|
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;
|
|
ctx.fillRect(x * res, 256 - res - y * res, res, res);
|
|
}
|
|
}
|
|
},
|
|
|
|
//
|
|
// 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.map !== undefined) {
|
|
this.setMap(options.map);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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_();
|
|
};
|
|
|
|
/**
|
|
* @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_);
|
|
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_);
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* 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
|
|
},
|
|
|
|
initialize: function (options) {
|
|
var self = this;
|
|
//this.project = this._project.bind(this);
|
|
this.render = this.render.bind(this);
|
|
L.Util.setOptions(this, options);
|
|
this._canvas = document.createElement('canvas');
|
|
this._ctx = this._canvas.getContext('2d');
|
|
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
|
|
this.requestAnimationFrame = requestAnimationFrame;
|
|
},
|
|
|
|
onAdd: function (map) {
|
|
this._map = map;
|
|
|
|
this._staticPane = map._createPane('leaflet-tile-pane', map._container);
|
|
this._staticPane.appendChild(this._canvas);
|
|
|
|
map.on({
|
|
'viewreset': this._reset
|
|
//'move': this._render
|
|
}, this);
|
|
|
|
map.on('move', this._render, this);//function(){ console.log("a"); }, this);
|
|
|
|
if(this.options.tileLoader) {
|
|
this._initTileLoader();
|
|
}
|
|
|
|
this._reset();
|
|
},
|
|
|
|
getCanvas: function() {
|
|
return this._canvas;
|
|
},
|
|
|
|
draw: function() {
|
|
return this._reset();
|
|
},
|
|
|
|
onRemove: function (map) {
|
|
map._container.removeChild(this._staticPane);
|
|
map.off({
|
|
'viewreset': this._reset,
|
|
'move': this._render
|
|
}, this);
|
|
},
|
|
|
|
addTo: function (map) {
|
|
map.addLayer(this);
|
|
return this;
|
|
},
|
|
|
|
setOpacity: function (opacity) {
|
|
this.options.opacity = opacity;
|
|
this._updateOpacity();
|
|
return this;
|
|
},
|
|
|
|
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;
|
|
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() {
|
|
this.requestAnimationFrame.call(window, this.render);
|
|
},
|
|
|
|
redraw: function() {
|
|
this._render();
|
|
},
|
|
|
|
onResize: function() {
|
|
},
|
|
|
|
render: function() {
|
|
throw new Error('render function should be implemented');
|
|
}
|
|
|
|
});
|
|
|
|
L.Mixin.TileLoader = {
|
|
|
|
_initTileLoader: function() {
|
|
this._tiles = {}
|
|
this._tilesToLoad = 0;
|
|
this._map.on({
|
|
'moveend': this._updateTiles
|
|
}, this);
|
|
this._updateTiles();
|
|
},
|
|
|
|
_removeTileLoader: function() {
|
|
map.off({
|
|
'moveend': this._updateTiles
|
|
}, this);
|
|
//TODO: remove tiles
|
|
},
|
|
|
|
_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);
|
|
},
|
|
|
|
_removeOtherTiles: function (bounds) {
|
|
var kArr, x, y, key;
|
|
|
|
for (key in this._tiles) {
|
|
if (this._tiles.hasOwnProperty(key)) {
|
|
kArr = key.split(':');
|
|
x = parseInt(kArr[0], 10);
|
|
y = parseInt(kArr[1], 10);
|
|
|
|
// remove tile if it's out of bounds
|
|
if (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];
|
|
},
|
|
|
|
_tileShouldBeLoaded: function (tilePoint) {
|
|
return !((tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom) in this._tiles);
|
|
},
|
|
|
|
_tileLoaded: function(tilePoint, tileData) {
|
|
this._tilesToLoad--;
|
|
this._tiles[tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom] = tileData;
|
|
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++) {
|
|
this.fire('tileAdded', queue[i]);
|
|
}
|
|
|
|
}
|
|
}
|
|
/**
|
|
* torque layer
|
|
*/
|
|
L.TorqueLayer = L.CanvasLayer.extend({
|
|
|
|
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;
|
|
options.tileLoader = true;
|
|
this.key = 0;
|
|
|
|
L.CanvasLayer.prototype.initialize.call(this, options);
|
|
|
|
this.options.renderer = this.options.renderer || 'point';
|
|
|
|
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) {
|
|
self._tileLoaded(t, tileData);
|
|
self.redraw();
|
|
});
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* render the selectef key
|
|
* don't call this function directly, it's called by
|
|
* requestAnimationFrame. Use redraw to refresh it
|
|
*/
|
|
render: function() {
|
|
var t, tile, pos;
|
|
var canvas = this.getCanvas();
|
|
canvas.width = canvas.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
if(typeof this.key === 'number') {
|
|
// renders only a "frame"
|
|
for(t in this._tiles) {
|
|
tile = this._tiles[t];
|
|
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);
|
|
}
|
|
} else {
|
|
// accumulate more than one
|
|
for(t in this._tiles) {
|
|
tile = this._tiles[t];
|
|
pos = this.getTilePos(tile.coord);
|
|
var accum = this.renderer.accumulate(tile, this.key);
|
|
ctx.setTransform(1, 0, 0, 1, pos.x, pos.y);
|
|
this.renderer.renderTileAccum(accum, 0, 0);
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* 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.redraw();
|
|
}
|
|
|
|
});
|
|
|
|
|
|
L.TiledTorqueLayer = L.TileLayer.Canvas.extend({
|
|
|
|
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;
|
|
|
|
options.async = true;
|
|
L.TileLayer.Canvas.prototype.initialize.call(this, options);
|
|
|
|
|
|
this.options.renderer = this.options.renderer || 'pixel';
|
|
|
|
this.provider = new this.providers[this.options.provider](options);
|
|
this.renderer = new this.renderers[this.options.renderer](null, options);
|
|
|
|
},
|
|
|
|
_tileLoaded: function(tile, tilePoint, tileData) {
|
|
if(this._tiles[tilePoint.x + ':' + tilePoint.y] !== undefined) {
|
|
this._tiles[tilePoint.x + ':' + tilePoint.y].data = tileData;
|
|
this.drawTile(tile);
|
|
}
|
|
L.TileLayer.Canvas.prototype._tileLoaded.call(this);
|
|
},
|
|
|
|
redraw: function() {
|
|
for (var i in this._tiles) {
|
|
this._redrawTile(this._tiles[i]);
|
|
}
|
|
},
|
|
|
|
_loadTile: function(tile, tilePoint) {
|
|
var self = this;
|
|
L.TileLayer.Canvas.prototype._loadTile.apply(this, arguments);
|
|
this.provider.getTileData(tilePoint, this._map.getZoom(), function(tileData) {
|
|
self._tileLoaded(tile, tilePoint, tileData);
|
|
L.DomUtil.addClass(tile, 'leaflet-tile-loaded');
|
|
});
|
|
},
|
|
|
|
drawTile: function (tile) {
|
|
var canvas = tile;
|
|
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();
|
|
}
|
|
|
|
});
|
|
|