Compare commits
17 Commits
master
...
bi_provide
Author | SHA1 | Date | |
---|---|---|---|
|
b7f38d5996 | ||
|
164643d0f1 | ||
|
e65367e8a6 | ||
|
1fa44a8ae1 | ||
|
87b93e6f9e | ||
|
dbf48ea091 | ||
|
6dd0251be8 | ||
|
19e35ffe2c | ||
|
7a1f206d5e | ||
|
1ec2324b83 | ||
|
e0606ee295 | ||
|
695cab290a | ||
|
ff4809b08c | ||
|
4ad0dba547 | ||
|
71fc89a8d8 | ||
|
fd5bc0f732 | ||
|
130d72c872 |
88
examples/bi.html
Normal file
88
examples/bi.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<link rel="stylesheet" href="vendor/leaflet.css" />
|
||||||
|
<style>
|
||||||
|
#map, html, body {
|
||||||
|
width: 100%; height: 100%; padding: 0; margin: 0;
|
||||||
|
}
|
||||||
|
#title {
|
||||||
|
position: absolute;
|
||||||
|
top: 100px;
|
||||||
|
left: 50px;
|
||||||
|
color: white;
|
||||||
|
font-size: 27px;
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<div id='graphs'></div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
|
||||||
|
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
|
||||||
|
<script src="../dist/torque.full.uncompressed.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// define the torque layer style using cartocss
|
||||||
|
// this creates a kind of density map
|
||||||
|
//color scale from http://colorbrewer2.org/
|
||||||
|
var CARTOCSS = [
|
||||||
|
'Map {',
|
||||||
|
'-torque-time-attribute: "tpep_dropoff_datetime";',
|
||||||
|
'-torque-aggregation-function: "avg(temp::float)";',
|
||||||
|
'-torque-frame-count: 1;',
|
||||||
|
'-torque-animation-duration: 15;',
|
||||||
|
'-torque-resolution: 1',
|
||||||
|
'}',
|
||||||
|
'#layer {',
|
||||||
|
' marker-width: 1;',
|
||||||
|
' marker-fill-opacity: 1.0;',
|
||||||
|
' marker-fill: #fff5eb; ',
|
||||||
|
' marker-type: ellipse;',
|
||||||
|
' [value > 1] { marker-fill: #fee6ce; }',
|
||||||
|
' [value > 2] { marker-fill: #fdd0a2; }',
|
||||||
|
' [value > 4] { marker-fill: #fdae6b; }',
|
||||||
|
' [value > 10] { marker-fill: #fd8d3c; }',
|
||||||
|
' [value > 15] { marker-fill: #f16913; }',
|
||||||
|
' [value > 20] { marker-fill: #d94801; }',
|
||||||
|
' [value > 25] { marker-fill: #8c2d04; }',
|
||||||
|
'}'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
|
||||||
|
var map = new L.Map('map', {
|
||||||
|
zoomControl: true,
|
||||||
|
center: [40.76045572900912, -73.97601127624512],
|
||||||
|
zoom: 13
|
||||||
|
});
|
||||||
|
|
||||||
|
L.tileLayer('http://{s}.api.cartocdn.com/base-dark/{z}/{x}/{y}.png', {
|
||||||
|
attribution: 'CartoDB'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
var torqueLayer = new L.TorqueLayer({
|
||||||
|
user : 'stuartlynn',
|
||||||
|
table : 'sampled_taxi_data',
|
||||||
|
cartocss: CARTOCSS,
|
||||||
|
provider: "filterable_sql_api"
|
||||||
|
});
|
||||||
|
torqueLayer.addTo(map);
|
||||||
|
|
||||||
|
var graphs=[]
|
||||||
|
// var ndx = crossfilter({});
|
||||||
|
//
|
||||||
|
// torqueLayer.onNewData(function(){
|
||||||
|
// data = torqueLayer.getValues()
|
||||||
|
// ndx = crossfilter(data)
|
||||||
|
// })
|
||||||
|
|
||||||
|
function addGraph(variable, type){
|
||||||
|
var dataDim = ndx.dimension(function(d){return d[variable]})
|
||||||
|
var graphID = "variable_graph"
|
||||||
|
$("graphs").append("<div id='"+graphID+"'></div>")
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -10,6 +10,7 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
|
|
||||||
providers: {
|
providers: {
|
||||||
'sql_api': torque.providers.json,
|
'sql_api': torque.providers.json,
|
||||||
|
'filterable_sql_api': torque.providers.filterableJson,
|
||||||
'url_template': torque.providers.JsonArray,
|
'url_template': torque.providers.JsonArray,
|
||||||
'windshaft': torque.providers.windshaft,
|
'windshaft': torque.providers.windshaft,
|
||||||
'tileJSON': torque.providers.tileJSON
|
'tileJSON': torque.providers.tileJSON
|
||||||
@ -28,6 +29,7 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
options.tileLoader = true;
|
options.tileLoader = true;
|
||||||
this.key = 0;
|
this.key = 0;
|
||||||
this.prevRenderedKey = 0;
|
this.prevRenderedKey = 0;
|
||||||
|
this._filters = {}
|
||||||
if (options.cartocss) {
|
if (options.cartocss) {
|
||||||
torque.extend(options, torque.common.TorqueLayer.optionsFromCartoCSS(options.cartocss));
|
torque.extend(options, torque.common.TorqueLayer.optionsFromCartoCSS(options.cartocss));
|
||||||
}
|
}
|
||||||
@ -69,8 +71,8 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
|
|
||||||
if (this.options.tileJSON) this.options.provider = 'tileJSON';
|
if (this.options.tileJSON) this.options.provider = 'tileJSON';
|
||||||
|
|
||||||
this.provider = new this.providers[this.options.provider](options);
|
this.provider = new this.providers[this.options.provider](this.options);
|
||||||
this.renderer = new this.renderers[this.options.renderer](this.getCanvas(), options);
|
this.renderer = new this.renderers[this.options.renderer](this.getCanvas(), this.options);
|
||||||
|
|
||||||
options.ready = function() {
|
options.ready = function() {
|
||||||
self.fire("change:bounds", {
|
self.fire("change:bounds", {
|
||||||
@ -98,11 +100,48 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
self.redraw();
|
self.redraw();
|
||||||
}
|
}
|
||||||
self.fire('tileLoaded');
|
self.fire('tileLoaded');
|
||||||
|
self.fire('dataUpdate')
|
||||||
});
|
});
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFilters: function() {
|
||||||
|
this.provider.setFilters(this._filters);
|
||||||
|
this._reloadTiles();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
filterByRange: function(variableName, start, end) {
|
||||||
|
this._filters[variableName] = {type: 'range', range: {start: start, end: end} }
|
||||||
|
this._filtersChanged()
|
||||||
|
this.fire('dataUpdate')
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
filterByCat: function(variableName, categories, exclusive) {
|
||||||
|
this._filters[variableName] = {type: 'cat', categories: categories, exclusive: !!exclusive };
|
||||||
|
this._filtersChanged()
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilter: function(name){
|
||||||
|
if(name) {
|
||||||
|
delete this._filters[name]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._filters = {}
|
||||||
|
}
|
||||||
|
this._filtersChanged()
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
_filtersChanged:function(){
|
||||||
|
this.provider._filters = this._filters;
|
||||||
|
this._clearTileCaches()
|
||||||
|
this._render()
|
||||||
|
},
|
||||||
|
|
||||||
_clearTileCaches: function() {
|
_clearTileCaches: function() {
|
||||||
var t, tile;
|
var t, tile;
|
||||||
for(t in this._tiles) {
|
for(t in this._tiles) {
|
||||||
@ -118,6 +157,96 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
this._clearTileCaches();
|
this._clearTileCaches();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
valuesForRangeVariable:function(variable){
|
||||||
|
var t, tile;
|
||||||
|
|
||||||
|
var variable_id = this.provider.idForRange(variable)
|
||||||
|
var values = [ ]
|
||||||
|
|
||||||
|
|
||||||
|
for(t in this._tiles){
|
||||||
|
tile = this._tiles[t]
|
||||||
|
var noPoints = tile.x.length;
|
||||||
|
for(var i=0; i < tile.x.length; i++){
|
||||||
|
if(tile.renderFlags[i] ){
|
||||||
|
value = tile.renderData[variable_id*noPoints + i]
|
||||||
|
values.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
},
|
||||||
|
|
||||||
|
valuesForCatVariable:function(variable){
|
||||||
|
var t, tile;
|
||||||
|
|
||||||
|
var categories = this.provider.idsForCategory(variable)
|
||||||
|
var result = [ ]
|
||||||
|
|
||||||
|
|
||||||
|
for(t in this._tiles){
|
||||||
|
tile = this._tiles[t]
|
||||||
|
var noPoints = tile.x.length;
|
||||||
|
for(var i=0; i < tile.x.length; i++){
|
||||||
|
if(tile.renderFlags[i] ){
|
||||||
|
var vals={}
|
||||||
|
Object.keys(categories).forEach(function(categoryName){
|
||||||
|
var variable_id = categories[categoryName]
|
||||||
|
value = tile.renderData[variable_id*noPoints + i]
|
||||||
|
vals[categoryName] = value
|
||||||
|
}.bind(this))
|
||||||
|
result.push(vals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
},
|
||||||
|
getValues:function(variable,callback){
|
||||||
|
var type= this.provider._mapping[variable].type
|
||||||
|
if(type=='float'){
|
||||||
|
callback(this.valuesForRangeVariable(variable))
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
callback(this.valuesForCatVariable(variable))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getHistogram:function(variable,callback,noBins){
|
||||||
|
var type= this.provider._mapping[variable].type
|
||||||
|
if(type=='float'){
|
||||||
|
callback(this.histogramForRangeVariable(variable,noBins))
|
||||||
|
}
|
||||||
|
else if(type='cat'){
|
||||||
|
callback(this.histogramForCatVariable(variable))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
histogramForCatVariable:function(variable){
|
||||||
|
var result = {}
|
||||||
|
this.valuesForCatVariable(variable).forEach(function(point){
|
||||||
|
Object.keys(point).forEach(function(key){
|
||||||
|
result[key] = result[key] || 0
|
||||||
|
result[key] += point[key]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
histogramForRangeVariable:function(variable,noBins){
|
||||||
|
noBins = noBins || 10
|
||||||
|
|
||||||
|
var vals = this.valuesForRangeVariable(variable)
|
||||||
|
|
||||||
|
var min = Math.min.apply(null, vals)
|
||||||
|
var max = Math.max.apply(null, vals)
|
||||||
|
var binSize = (max-min)/noBins
|
||||||
|
var result = []
|
||||||
|
vals.forEach(function(val){
|
||||||
|
var bin = (val -min)/binSize
|
||||||
|
result[bin]= result[bin] || 0
|
||||||
|
result[bin] += val
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
onAdd: function (map) {
|
onAdd: function (map) {
|
||||||
map.on({
|
map.on({
|
||||||
'zoomend': this._clearCaches,
|
'zoomend': this._clearCaches,
|
||||||
@ -208,6 +337,62 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
canvas.width = canvas.width;
|
canvas.width = canvas.width;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDataForPoint:function(x,y,callback, maxNo, tolerance){
|
||||||
|
var maxNo = maxNo || 10
|
||||||
|
var tolerance = tolerance || 10
|
||||||
|
|
||||||
|
for(var t in this._tiles ){
|
||||||
|
tile = this._tiles[t];
|
||||||
|
pos = this.getTilePos(tile.coord);
|
||||||
|
|
||||||
|
var tileWidth = 255.0/this.options.resolution
|
||||||
|
|
||||||
|
xx = x - pos.x;
|
||||||
|
yy = y - pos.y;
|
||||||
|
|
||||||
|
if(xx >= 0 && yy >= 0 && xx < 255 && yy <= 255) {
|
||||||
|
this.provider.getDataForTorquePixel(tile.coord,xx,yy,maxNo,tolerance,callback)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(null)
|
||||||
|
return this
|
||||||
|
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
_filterTile:function(tile){
|
||||||
|
var noPoints = tile.x.length
|
||||||
|
|
||||||
|
var renderFlags = []
|
||||||
|
for(var i =0; i < noPoints; i++){
|
||||||
|
var includePoint = true
|
||||||
|
Object.keys(this._filters).forEach(function(key){
|
||||||
|
var filter = this._filters[key]
|
||||||
|
var variableId = this.provider.idForRange(key)
|
||||||
|
|
||||||
|
var value = tile.renderData[variableId*noPoints+i]
|
||||||
|
if(filter.type=='range'){
|
||||||
|
if(value < filter.range.start || value > filter.range.end){
|
||||||
|
includePoint = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (filter.type=='cat'){
|
||||||
|
var ids = this.provider.idsForCategory(key);
|
||||||
|
filter.categories.forEach(function(key){
|
||||||
|
var catId = ids[key]
|
||||||
|
var value = tile.renderData[catId*noPoints + i]
|
||||||
|
if(value==0){
|
||||||
|
includePoint= false
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
renderFlags[i] = includePoint
|
||||||
|
}
|
||||||
|
return renderFlags
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* render the selectef key
|
* render the selectef key
|
||||||
* don't call this function directly, it's called by
|
* don't call this function directly, it's called by
|
||||||
@ -236,10 +421,13 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
// all the points
|
// all the points
|
||||||
this.renderer._ctx.drawImage(tile._tileCache, 0, 0);
|
this.renderer._ctx.drawImage(tile._tileCache, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
//tile.renderFlags = this._filterTile(tile)
|
||||||
this.renderer.renderTile(tile, this.key);
|
this.renderer.renderTile(tile, this.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderer.applyFilters();
|
this.renderer.applyFilters();
|
||||||
|
|
||||||
// prepare caches if the animation is not running
|
// prepare caches if the animation is not running
|
||||||
@ -378,11 +566,26 @@ L.TorqueLayer = L.CanvasLayer.extend({
|
|||||||
*/
|
*/
|
||||||
getValues: function(step) {
|
getValues: function(step) {
|
||||||
var values = [];
|
var values = [];
|
||||||
|
var idx = 0;
|
||||||
|
var mappedValues = [];
|
||||||
step = step === undefined ? this.key: step;
|
step = step === undefined ? this.key: step;
|
||||||
var t, tile;
|
var t, tile;
|
||||||
for(t in this._tiles) {
|
for(t in this._tiles) {
|
||||||
tile = this._tiles[t];
|
tile = this._tiles[t];
|
||||||
this.renderer.getValues(tile, step, values);
|
if (tile) {
|
||||||
|
this.renderer.getValues(tile, step, values);
|
||||||
|
// map the categories
|
||||||
|
var mapping = tile.categories[step];
|
||||||
|
if (mapping) {
|
||||||
|
for (var i = idx; i <= values.length - idx; ++i) {
|
||||||
|
mappedValues.push(mapping[values[i]]);
|
||||||
|
}
|
||||||
|
idx = values.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mappedValues.length) {
|
||||||
|
return mappedValues;
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
},
|
},
|
||||||
|
661
lib/torque/provider/filterableJson.js
Normal file
661
lib/torque/provider/filterableJson.js
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
var torque = require('../');
|
||||||
|
var _ = require('underscore');
|
||||||
|
var Profiler = require('../profiler');
|
||||||
|
|
||||||
|
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 filterableJson = function (options) {
|
||||||
|
this._ready = false;
|
||||||
|
this._tileQueue = [];
|
||||||
|
this.options = options;
|
||||||
|
this._filters = {};
|
||||||
|
this._tileProcessingQueue=[]
|
||||||
|
this._workers = [];
|
||||||
|
this._maxWorkerNo = this.options.maxWorkerNo || 4;
|
||||||
|
|
||||||
|
this.setupWorkerPool()
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// check options
|
||||||
|
if (options.resolution === undefined ) throw new Error("resolution should be provided");
|
||||||
|
if(options.start === undefined) {
|
||||||
|
this._fetchKeySpan();
|
||||||
|
} else {
|
||||||
|
this._setReady(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
filterableJson.prototype = {
|
||||||
|
|
||||||
|
setupWorkerPool:function(){
|
||||||
|
for(var i=0; i< this._maxWorkerNo; i++){
|
||||||
|
this._workers.push(this.createProccessTileWorker())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAvalaibleWorker:function(){
|
||||||
|
return this._workers.pop()
|
||||||
|
},
|
||||||
|
|
||||||
|
releaseWorker:function(worker){
|
||||||
|
console.log("releasing worker ", worker)
|
||||||
|
this._workers.push(worker)
|
||||||
|
this.processNextTileRequestInQueue()
|
||||||
|
},
|
||||||
|
|
||||||
|
processNextTileRequestInQueue:function(){
|
||||||
|
console.log("processing next ",this._tileProcessingQueue.length, this._workers.length )
|
||||||
|
if(this._tileProcessingQueue.length>0){
|
||||||
|
job = this._tileProcessingQueue.pop()
|
||||||
|
this.requestWorker(job.rows,job.coord,job.zoom, job.options, job.callback)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
requestWorker:function(rows,coord,zoom,options,callback){
|
||||||
|
worker = this.getAvalaibleWorker()
|
||||||
|
self = this
|
||||||
|
if(worker){
|
||||||
|
worker.onmessage = function(e){
|
||||||
|
callback(e.data)
|
||||||
|
self.releaseWorker(this)
|
||||||
|
}
|
||||||
|
worker.postMessage(JSON.stringify({rows: rows, coord: {x:coord.x,y:coord.y}, zoom:zoom, options: options}))
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.addToTileProcessingQueue(rows,coord,zoom,options,callback)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addToTileProcessingQueue:function(rows,coord,zoom, options, callback){
|
||||||
|
this._tileProcessingQueue.push({rows:rows, coord:coord, zoom:zoom, options: options, callback:callback})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a worker to process the tile
|
||||||
|
*/
|
||||||
|
createProccessTileWorker: function(){
|
||||||
|
var workerFunction = "var proccessTile ="+ this.proccessTileSerial.toString()
|
||||||
|
var wrapper = "; self.onmessage = function(e){var data = JSON.parse(e.data); JSON.stringify(self.postMessage(proccessTile(data.rows,data.coord, data.zoom, data.options)))}"
|
||||||
|
var script = workerFunction + wrapper;
|
||||||
|
var blob = new Blob([script], {type: "text/javascript"})
|
||||||
|
var worker = new Worker(window.URL.createObjectURL(blob))
|
||||||
|
return worker
|
||||||
|
},
|
||||||
|
|
||||||
|
proccessTile: function(rows, coord, zoom, callback){
|
||||||
|
var self = this;
|
||||||
|
if(typeof(Worker) === "undefined"){
|
||||||
|
callback(this.proccessTileSerial(rows,coord,zoom, this.options))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var workerSafeOptions = {
|
||||||
|
resolution: this.options.resolution,
|
||||||
|
fields: this.options.fields
|
||||||
|
}
|
||||||
|
this.requestWorker(rows, coord, zoom, workerSafeOptions, callback)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
proccessTileSerial: function(rows, coord, zoom, options) {
|
||||||
|
// utility function for hashing categories
|
||||||
|
var r;
|
||||||
|
var x = new Uint8Array(rows.length);
|
||||||
|
var y = new Uint8Array(rows.length);
|
||||||
|
|
||||||
|
if(typeof(Profiler) != 'undefined') {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryMapping = {}
|
||||||
|
var categoryMappingSize = {}
|
||||||
|
var fields = options.fields;
|
||||||
|
|
||||||
|
for (var i = 0 ; i < fields.length; ++i) {
|
||||||
|
if (fields[i].type === 'cat') {
|
||||||
|
categoryMapping[i] = {};
|
||||||
|
categoryMappingSize[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// count number of steps
|
||||||
|
var maxDateSlots = Object.keys(rows[0].d).length;
|
||||||
|
var steps = maxDateSlots;
|
||||||
|
|
||||||
|
|
||||||
|
// reserve memory for all the steps
|
||||||
|
var timeIndex = new Int32Array(maxDateSlots + 1); //index-size
|
||||||
|
var timeCount = new Int32Array(maxDateSlots + 1);
|
||||||
|
var renderData = new Float32Array(rows.length * steps); //(this.options.valueDataType || type)(steps);
|
||||||
|
var renderDataPos = new Uint32Array(rows.length * steps);
|
||||||
|
|
||||||
|
if(typeof(Profiler) !='undefined'){
|
||||||
|
prof_mem.inc(
|
||||||
|
4 * maxDateSlots + // timeIndex
|
||||||
|
4 * maxDateSlots + // timeCount
|
||||||
|
steps + //renderData
|
||||||
|
steps * 4
|
||||||
|
); //renderDataPos
|
||||||
|
|
||||||
|
prof_point_count.inc(rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowsPerSlot = {};
|
||||||
|
// var steps = _.range(maxDateSlots);
|
||||||
|
var steps = []
|
||||||
|
|
||||||
|
for(var i=0 ; i< maxDateSlots; i++){
|
||||||
|
steps.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// precache pixel positions
|
||||||
|
for (var r = 0; r < rows.length; ++r) {
|
||||||
|
var row = rows[r];
|
||||||
|
x[r] = row.x * options.resolution;
|
||||||
|
// fix value when it's in the tile EDGE
|
||||||
|
// TODO: this should be fixed in SQL query
|
||||||
|
if (row.y === -1) {
|
||||||
|
y[r] = 0;
|
||||||
|
} else {
|
||||||
|
y[r] = row.y * options.resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
var vals = row.d;
|
||||||
|
|
||||||
|
for (var j = 0, len = steps.length; j < len; ++j) {
|
||||||
|
var rr = rowsPerSlot[steps[j]] || (rowsPerSlot[steps[j]] = []);
|
||||||
|
var k = 'f' + (j + 1)
|
||||||
|
var v = vals[k];
|
||||||
|
if (options.fields[j].type === 'cat') {
|
||||||
|
var mapping = categoryMapping[j];
|
||||||
|
var m = mapping[v]
|
||||||
|
if (!m) {
|
||||||
|
var count = ++categoryMappingSize[j];
|
||||||
|
v = mapping[v] = count;
|
||||||
|
} else {
|
||||||
|
v = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rr.push([r, v]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof(Profiler) !='undefined'){
|
||||||
|
prof_process_time.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// invert the mapping
|
||||||
|
var invertedMapping = {}
|
||||||
|
for (var i = 0 ; i < fields.length; ++i) {
|
||||||
|
if (fields[i].type === 'cat') {
|
||||||
|
var cat = categoryMapping[i];
|
||||||
|
invertedMapping[i] = {}
|
||||||
|
for (var k in cat) {
|
||||||
|
invertedMapping[i][cat[k]] = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
categories: invertedMapping
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_generateFilterSQLForCat: function(name, categories, exclusive) {
|
||||||
|
return name + (exclusive ? " not in ": " in") + '(' + categories.map(function(c) {
|
||||||
|
// escape ', double escape. one for javascript, another one for postgres
|
||||||
|
c = c.replace("'", "''''");
|
||||||
|
return "''" + c + "''";
|
||||||
|
}).join(',') + ')';
|
||||||
|
},
|
||||||
|
|
||||||
|
_generateFilterSQLForRange: function(name,range) {
|
||||||
|
var result = ""
|
||||||
|
if (range.start) {
|
||||||
|
result += " " + name + " > " + range.start;
|
||||||
|
}
|
||||||
|
if (range.end) {
|
||||||
|
if (range.start) {
|
||||||
|
result += " and "
|
||||||
|
}
|
||||||
|
result += " " + name + " < " + range.end;
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
_setFilters:function(filters){
|
||||||
|
this.filters = filters
|
||||||
|
},
|
||||||
|
|
||||||
|
_generateFiltersSQL: function() {
|
||||||
|
var self = this;
|
||||||
|
return Object.keys(this._filters).map(function(filterName){
|
||||||
|
var filter = self._filters[filterName]
|
||||||
|
if (filter) {
|
||||||
|
if (filter.type == 'range') {
|
||||||
|
return self._generateFilterSQLForRange(filterName, filter.range)
|
||||||
|
}
|
||||||
|
else if (filter.type == 'cat' && filter.categories.length) {
|
||||||
|
return self._generateFilterSQLForCat(filterName, filter.categories, filter.exclusive)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}).filter(function(f) {
|
||||||
|
return f.length > 0;
|
||||||
|
}).map(function(f) {
|
||||||
|
return "(" + f + ")";
|
||||||
|
}).join(' and ')
|
||||||
|
},
|
||||||
|
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getTableSQL: function(coord, zoom) {
|
||||||
|
return format("(WITH opt as ( SELECT table_name as tt FROM selectivity(x:={x}, y:={y}, z:={z}, tables:=ARRAY[{overview_tables}], where_clause:='{filters}') where nrows > 5000 order by nrows asc limit 1) select (CASE WHEN EXISTS(select 1 from opt) THEN (select tt from opt) else '{table}' END))", {
|
||||||
|
overview_tables: this.options.overview_tables.map(function(t) { return "'" + t + "'"; }).join(','),
|
||||||
|
table: this.options.table,
|
||||||
|
z: zoom,
|
||||||
|
x: coord.x,
|
||||||
|
y: coord.y,
|
||||||
|
filters: this._generateFiltersSQL()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `tile` the tile the point resides on
|
||||||
|
* `x` the x pixel coord on the tile
|
||||||
|
* 'y' the y pixel coord on the tile
|
||||||
|
* 'maxNo' the maximum number of points to return
|
||||||
|
* 'pixel tollerance around click' How many pixels to search around the click point
|
||||||
|
* 'callback' function(rows) returns an array of the data for that point
|
||||||
|
*/
|
||||||
|
getDataForTorquePixel:function(tile,x,y,maxNo,tolerance,callback){
|
||||||
|
shift = 23 - tile.z
|
||||||
|
tolerance = tolerance || 20
|
||||||
|
var sql =""+
|
||||||
|
"with qr as (select * from xyz2range({x},{y},{z})) "+
|
||||||
|
"select *, ((quadkey_x & (255 << {shift})) >> {shift}) AS torque_tile_x, "+
|
||||||
|
"(255 - ((quadkey_y & (255 << {shift})) >> {shift})) AS torque_tile_y "+
|
||||||
|
"from {table}, qr "+
|
||||||
|
"where (quadkey between qr.min and qr.max) "+
|
||||||
|
"and ((((quadkey_x & (255 << {shift})) >> {shift}) - {torque_tile_x}) between -{tolerance} and {tolerance}) and (((255 - ((quadkey_y & (255 << {shift})) >> {shift})) - {torque_tile_y}) between -{tolerance} and {tolerance}) "+
|
||||||
|
"limit {maxNo}"
|
||||||
|
|
||||||
|
var query = format(sql,{
|
||||||
|
x: tile.x,
|
||||||
|
y: tile.y,
|
||||||
|
z: tile.z,
|
||||||
|
table: this.options.table,
|
||||||
|
torque_tile_x: x,
|
||||||
|
torque_tile_y: y,
|
||||||
|
maxNo: maxNo,
|
||||||
|
shift: shift,
|
||||||
|
tolerance: tolerance
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sql(query,function(data){
|
||||||
|
if(data){
|
||||||
|
var rows = JSON.parse(data.responseText).rows;
|
||||||
|
callback(rows)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `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;
|
||||||
|
|
||||||
|
var sql = "select * from torque_tile_json({x}, {y}, {zoom}, ARRAY[{fields}], {table}, '{filters}')";
|
||||||
|
|
||||||
|
var query = format(sql, {
|
||||||
|
zoom: zoom,
|
||||||
|
x: coord.x,
|
||||||
|
y: coord.y,
|
||||||
|
fields: _.map(this.options.fields, function(f) {
|
||||||
|
if (f.type === 'cat') {
|
||||||
|
return "'mode() within group (order by " + f.name + ")'";
|
||||||
|
}
|
||||||
|
return "'avg(" + f.name + ")'";
|
||||||
|
}).join(','),
|
||||||
|
column: column_conv,
|
||||||
|
table: this._getTableSQL(coord, zoom),
|
||||||
|
filters: this._generateFiltersSQL()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.sql(query, function (data) {
|
||||||
|
if (data) {
|
||||||
|
var rows = JSON.parse(data.responseText).rows;
|
||||||
|
if (rows.length !== 0) {
|
||||||
|
self.proccessTile(rows, coord, zoom,callback);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
} 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) : "");
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchKeySpan: function() {
|
||||||
|
this._setReady(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
getHistogram: function(varName, callback) {
|
||||||
|
|
||||||
|
var sql = [
|
||||||
|
'with width as (',
|
||||||
|
'select min({column}) as min,',
|
||||||
|
'max({column}) as max,',
|
||||||
|
'20 as buckets',
|
||||||
|
'from {table}',
|
||||||
|
'),',
|
||||||
|
'_bw as ( select (max - min)/buckets as bw from width ),',
|
||||||
|
'histogram as (',
|
||||||
|
'select width_bucket({column}, min, max, buckets) as bucket,',
|
||||||
|
'numrange(min({column})::numeric, max({column})::numeric, \'[]\') as range,',
|
||||||
|
'count(*) as freq',
|
||||||
|
'from {table}, width ',
|
||||||
|
//'where trip_time_in_secs between min and max',
|
||||||
|
'group by bucket',
|
||||||
|
'order by bucket',
|
||||||
|
')',
|
||||||
|
'select bucket*bw as start, (bucket+1)*bw as end, bucket as bin, lower(range) as min, upper(range) as max, freq from histogram, _bw;'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
var query = format(sql.join('\n'), this.options, {
|
||||||
|
column: this.options.column,
|
||||||
|
table: this.options.table,
|
||||||
|
filters: this._generateFiltersSQL()
|
||||||
|
});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.sql(query, function (data) {
|
||||||
|
if (data) {
|
||||||
|
var rows = JSON.parse(data.responseText).rows;
|
||||||
|
callback(null, rows);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = filterableJson;
|
@ -1,5 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
json: require('./json'),
|
json: require('./json'),
|
||||||
|
filterableJson: require('./filterableJson'),
|
||||||
JsonArray: require('./jsonarray'),
|
JsonArray: require('./jsonarray'),
|
||||||
windshaft: require('./windshaft'),
|
windshaft: require('./windshaft'),
|
||||||
tileJSON: require('./tilejson')
|
tileJSON: require('./tilejson')
|
||||||
|
@ -57,7 +57,7 @@ var Filters = require('./torque_filters');
|
|||||||
this.TILE_SIZE = 256;
|
this.TILE_SIZE = 256;
|
||||||
this._style = null;
|
this._style = null;
|
||||||
this._gradients = {};
|
this._gradients = {};
|
||||||
|
|
||||||
this._forcePoints = false;
|
this._forcePoints = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,14 +165,14 @@ var Filters = require('./torque_filters');
|
|||||||
i.src = canvas.toDataURL();
|
i.src = canvas.toDataURL();
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
// renders all the layers (and frames for each layer) from cartocss
|
// renders all the layers (and frames for each layer) from cartocss
|
||||||
//
|
//
|
||||||
renderTile: function(tile, key, callback) {
|
renderTile: function(tile, key, renderFlags, callback) {
|
||||||
if (this._iconsToLoad > 0) {
|
if (this._iconsToLoad > 0) {
|
||||||
this.on('allIconsLoaded', function() {
|
this.on('allIconsLoaded', function() {
|
||||||
this.renderTile.apply(this, [tile, key, callback]);
|
this.renderTile.apply(this, [tile, key, callback]);
|
||||||
@ -193,7 +193,7 @@ var Filters = require('./torque_filters');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prof.end(true);
|
prof.end(true);
|
||||||
|
|
||||||
return callback && callback(null);
|
return callback && callback(null);
|
||||||
@ -237,12 +237,11 @@ var Filters = require('./torque_filters');
|
|||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
// renders a tile in the canvas for key defined in
|
// renders a tile in the canvas for key defined in
|
||||||
// the torque tile
|
// the torque tile
|
||||||
//
|
//
|
||||||
_renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars) {
|
_renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars) {
|
||||||
if (!this._canvas) return;
|
if (!this._canvas) return;
|
||||||
|
|
||||||
var prof = Profiler.metric('torque.renderer.point.renderTile').start();
|
var prof = Profiler.metric('torque.renderer.point.renderTile').start();
|
||||||
var ctx = this._ctx;
|
var ctx = this._ctx;
|
||||||
var blendMode = compop2canvas(shader.eval('comp-op')) || this.options.blendmode;
|
var blendMode = compop2canvas(shader.eval('comp-op')) || this.options.blendmode;
|
||||||
@ -254,27 +253,25 @@ var Filters = require('./torque_filters');
|
|||||||
key = tile.maxDate;
|
key = tile.maxDate;
|
||||||
}
|
}
|
||||||
var tileMax = this.options.resolution * (this.TILE_SIZE/this.options.resolution - 1)
|
var tileMax = this.options.resolution * (this.TILE_SIZE/this.options.resolution - 1)
|
||||||
var activePixels = tile.timeCount[key];
|
var activePixels = tile.x.length;
|
||||||
var anchor = this.options.resolution/2;
|
var anchor = this.options.resolution/2;
|
||||||
if (activePixels) {
|
if (activePixels) {
|
||||||
var pixelIndex = tile.timeIndex[key];
|
var pixelIndex = 0;//tile.timeIndex[key];
|
||||||
for(var p = 0; p < activePixels; ++p) {
|
for(var p = 0; p < activePixels; ++p) {
|
||||||
var posIdx = tile.renderDataPos[pixelIndex + p];
|
var posIdx = tile.renderDataPos[pixelIndex + p];
|
||||||
var c = tile.renderData[pixelIndex + p];
|
var c = tile.renderData[pixelIndex + p];
|
||||||
if (c) {
|
var sp = sprites[c];
|
||||||
var sp = sprites[c];
|
if (sp === undefined) {
|
||||||
if (sp === undefined) {
|
sp = sprites[c] = this.generateSprite(shader, c, torque.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars));
|
||||||
sp = sprites[c] = this.generateSprite(shader, c, torque.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars));
|
}
|
||||||
}
|
if (sp) {
|
||||||
if (sp) {
|
var x = tile.x[posIdx]- (sp.width >> 1) + anchor;
|
||||||
var x = tile.x[posIdx]- (sp.width >> 1) + anchor;
|
var y = tileMax - tile.y[posIdx] + anchor; // flip mercator
|
||||||
var y = tileMax - tile.y[posIdx] + anchor; // flip mercator
|
ctx.drawImage(sp, x, y - (sp.height >> 1));
|
||||||
ctx.drawImage(sp, x, y - (sp.height >> 1));
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
prof.end(true);
|
prof.end(true);
|
||||||
},
|
},
|
||||||
@ -442,7 +439,7 @@ var Filters = require('./torque_filters');
|
|||||||
}
|
}
|
||||||
gradient = {};
|
gradient = {};
|
||||||
var colorize = this._style['image-filters'].args;
|
var colorize = this._style['image-filters'].args;
|
||||||
|
|
||||||
var increment = 1/colorize.length;
|
var increment = 1/colorize.length;
|
||||||
for (var i = 0; i < colorize.length; i++){
|
for (var i = 0; i < colorize.length; i++){
|
||||||
var key = increment * i + increment;
|
var key = increment * i + increment;
|
||||||
|
@ -1,160 +1,61 @@
|
|||||||
|
var torque = require('../');
|
||||||
|
var cartocss = require('./cartocss_render');
|
||||||
|
var Profiler = require('../profiler');
|
||||||
var carto = global.carto || require('carto');
|
var carto = global.carto || require('carto');
|
||||||
|
var Filters = require('./torque_filters');
|
||||||
|
var PointRenderer = require('./point')
|
||||||
|
|
||||||
var DEFAULT_CARTOCSS = [
|
var PixelRenderer = function(canvas, options) {
|
||||||
'#layer {',
|
PointRenderer.call(this, canvas, options);
|
||||||
' 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;
|
torque.extend(PixelRenderer.prototype, PointRenderer.prototype, {
|
||||||
|
|
||||||
//
|
generateSprite: function(shader, value, shaderVars) {
|
||||||
// this renderer just render points depending of the value
|
var self = this;
|
||||||
//
|
var prof = Profiler.metric('torque.renderer.point.generateSprite').start();
|
||||||
function RectanbleRenderer(canvas, options) {
|
var st = shader.getStyle({
|
||||||
this.options = options;
|
value: value
|
||||||
carto.tree.Reference.set(torque['torque-reference']);
|
}, shaderVars);
|
||||||
this.setCanvas(canvas);
|
if(this._style === null || this._style !== st){
|
||||||
this.setCartoCSS(this.options.cartocss || DEFAULT_CARTOCSS);
|
this._style = st;
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
return {
|
||||||
key = keys[k];
|
width: st['marker-width'],
|
||||||
activePixels = tile.timeCount[key];
|
color: st['marker-fill']
|
||||||
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) {
|
_renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars) {
|
||||||
var prof = Profiler.metric('RectangleRender:renderTileAccum').start();
|
if (!this._canvas) return;
|
||||||
var color, x, y, alpha;
|
var prof = Profiler.metric('torque.renderer.point.renderTile').start();
|
||||||
var res = this.options.resolution;
|
|
||||||
var ctx = this._ctx;
|
var ctx = this._ctx;
|
||||||
var s = (256/res) | 0;
|
if (this.options.cumulative && key > tile.maxDate) {
|
||||||
var s2 = s*s;
|
//TODO: precache because this tile is not going to change
|
||||||
var colors = this._colors;
|
key = tile.maxDate;
|
||||||
if(this.options.blendmode) {
|
|
||||||
ctx.globalCompositeOperation = this.options.blendmode;
|
|
||||||
}
|
}
|
||||||
var polygon_alpha = this._shader['polygon-opacity'] || function() { return 1.0; };
|
var tileMax = this.options.resolution * (this.TILE_SIZE/this.options.resolution - 1)
|
||||||
for(var i = 0; i < s2; ++i) {
|
var activePixels = tile.x.length;
|
||||||
var xy = i;
|
var anchor = this.options.resolution/2;
|
||||||
var value = accum[i];
|
if (activePixels) {
|
||||||
if(value) {
|
var pixelIndex = 0;//tile.timeIndex[key];
|
||||||
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, callback) {
|
|
||||||
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) {
|
for(var p = 0; p < activePixels; ++p) {
|
||||||
var posIdx = tile.renderDataPos[pixelIndex + p];
|
var posIdx = tile.renderDataPos[pixelIndex + p];
|
||||||
var c = tile.renderData[pixelIndex + p];
|
var c = tile.renderData[pixelIndex + p];
|
||||||
if(c) {
|
var sp = sprites[c];
|
||||||
var color = colors[Math.min(c, colors.length - 1)];
|
if (sp === undefined) {
|
||||||
var x = tile.x[posIdx];// + px;
|
sp = sprites[c] = this.generateSprite(shader, c, torque.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars));
|
||||||
var y = tile.y[posIdx]; //+ py;
|
}
|
||||||
|
if (sp) {
|
||||||
ctx.fillStyle = color;
|
var x = tile.x[posIdx]- (sp.width >> 1) + anchor;
|
||||||
ctx.fillRect(x, y, res, res);
|
var y = tileMax - tile.y[posIdx] + anchor; // flip mercator
|
||||||
/*
|
ctx.fillStyle = sp.color;
|
||||||
|
ctx.fillRect(x, y, sp.width, sp.width);
|
||||||
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();
|
|
||||||
return callback && callback(null);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
module.exports = PixelRenderer;
|
||||||
// exports public api
|
|
||||||
module.exports = RectanbleRenderer;
|
|
||||||
|
Loading…
Reference in New Issue
Block a user