Merge pull request #46 from CartoDB/interactivity

Interactivity
This commit is contained in:
javi santana 2014-09-24 12:27:34 +02:00
commit 7ea3709bee
8 changed files with 217 additions and 105 deletions

1
NEWS
View File

@ -5,6 +5,7 @@
- Add no_cdn option to windshaft provider
- Add getActivePointsBBox method
- Added renderer cache when the map is not animated
- Added interaction method
2.4.01
- fixed rectangle anchor and size #42

View File

@ -1,13 +1,6 @@
# Torque API
Torque provides two kinds of visualizations.
- static: provides a way to create heatmap like visualizations (note for Andrew: fon). see ``TorqueLayer``
- dynamic: animate points over a map (note for Andrew: Navy) see ``TiledTorqueLayer``
depending on the map provider you are using you need to use different layer type. Currently we provide layers for Google Maps and Leaflet.
## L.TorqueLayer(options)
@ -20,12 +13,7 @@ One of two core classes for the Torque library - it is used to create an animate
var torqueLayer = new L.TorqueLayer({
user : 'viz2',
table : 'ow',
column : 'date',
countby : 'count(cartodb_id)',
resolution : 1,
steps : 750,
pixel_size : 3,
blendmode : 'lighter'
cartocss: CARTOCSS
});
```
@ -34,37 +22,29 @@ One of two core classes for the Torque library - it is used to create an animate
##### Provider options
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| provider | string | ```sql_api``` | Where is the data coming from? Alternative is 'url_template'|
| provider | string | ```sql_api``` | Where is the data coming from |
##### CartoDB data options
##### CartoDB data options (SQL API provider)
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| user | string | ```null``` | CartoDB account name. Found as, accountname.cartodb.com|
| table | string | ```null``` | CartoDB table name where data is found |
| column | string | ```null``` | CartoDB table's column name where date information is found (for dynamic type torque layer only)|
| countby | string | ```null``` | The aggregation method to use for each pixel displayed where multiple data are found. Any valid PostgreSQL aggregate function |
| table | string | ```null``` | CartoDB table name where data is found |
| sql | string | ```null``` | SQL query to be performed to fetch the data. You must
use this param or table, not at the same time |
##### Display options
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| resolution| numeric | ```2``` | The x and y dimensions of each pixel as returned by the data|
| blendmode | boolean | ```source-over``` | The HTML5 Canvas composite operation for when multiple pixels overlap on the canvas |
##### Time options
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| steps | integer | ```100``` | The maximun number of steps to divide the data into for animated renderings |
| animationDuration | integer | ```null``` | time in seconds the animation last |
### Time methods
| Method | options | returns | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| ```setStep(step)``` | ```time numeric``` | ```this``` | sets the animation to the step indicated by ```step```, must be between 0 and ```steps```|
| ```play```| | ```this```| starts the animation
| ```stop```| | ```this```| stops the animation and set time to step 0
| ```pause```| | ```this```| stops the animation but keep the current time (play enables the animation again)
| ```play()```| | ```this```| starts the animation
| ```stop()```| | ```this```| stops the animation and set time to step 0
| ```pause()```| | ```this```| stops the animation but keep the current time (play enables the animation again)
| ```toggle()```| | ```this```| toggles (pause/play) the animation
| ```getStep()``` | | current animation step (integer) | gets the current animation step
| ```getTime()``` | | current animation time (Date) | gets the real animation time
### Style methods
@ -97,63 +77,16 @@ This should be ```string``` encoded in Javascript
}
```
## L.TiledTorqueLayer(options)
One of two core classes for the Torque library - it is used to create a static torque layer with client side filtering.
##### Provider options
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| provider | string | ```sql_api``` | Where is the data coming from? Alternative is 'url_template'|
| url | string | ```null``` | Tile template URL for fetching data e.g 'http://host.com/{z}/{x}/{y}.json'|
##### CartoDB data options (Note to Santana: are these really here?)
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| user | string | ```null``` | CartoDB account name. Found as, accountname.cartodb.com|
| table | string | ```null``` | CartoDB table name where data is found |
| column | string | ```null``` | CartoDB table's column name where date information is found (for dynamic type torque layer only)|
| countby | string | ```null``` | The aggregation method to use for each pixel displayed where multiple data are found. Any valid PostgreSQL aggregate function |
##### Display options (Note to Santana: is blendmode here? or above even?)
| Option | type | Default | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| resolution| numeric | ```2``` | The x and y dimensions of each pixel as returned by the data|
| blendmode | boolean | ```source-over``` | The HTML5 Canvas composite operation for when multiple pixels overlap on the canvas |
### Filtering options
### Interaction methods (only available for Leaflet)
| Method | options | returns | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| ```setKey(keys)``` | ```keys numeric/array``` | ```this``` | which data categories to display on the map |
| ```setSQL(sql)``` | ```sql string ``` | ```this``` | by default sql torque layer uses is
"select * from table", this method changes the sql query torque uses to fetch the data |
| ```getValueForPos(x, y[, step])```| | an object like { bbox:[], value: VALUE } if there is value
for the pos, null otherwise | allows to get the value for the coordinate (in map reference system)
for a concrete step. If step is not specified the animation one is used. This method is expensive
in terms of CPU so be careful. It returns the value from the raster data not the rendered data |
| ```getActivePointsBBox(step)```| | list of bbox | returns the list of bounding boxes active for
``step``
### Style options
| Method | options | returns | Description |
|-----------|:-----------|:----------|:---------------------------------------|
| ```setCartoCSS(cartocss)``` | ```cartocss string``` | ```this``` | style the map rendering using client-side cartocss |
``value`` and ``zoom`` variables can be used. only ``polygon-fill`` and ``polygon-opacity`` properties are supported currently. To see the full list of supported parameters, read the [Torque CartoCSS documentation here](CartoCSS.md).
TorqueLayer currently expects ```polygon``` styling
##### CartoCSS Example
This should be ```string``` encoded in Javascript
```css
#layer {
polygon-fill: #FFFF00;
[value >= 10] { polygon-fill: #FFCC00; }
[value >= 100] { polygon-fill: #FF9900; }
[value >= 1000] { polygon-fill: #FF6600; }
[value >= 10000] { polygon-fill: #FF3300; }
[value > 100000] { polygon-fill: #C00; }
}
```
# Google Maps Layers
@ -171,19 +104,3 @@ is not a layer is a overlay so in order to add it to the map use ``layer.setMap`
see ``L.TorqueLayer`` for the rest of the options.
## GMapsTiledTorqueLayer(options)
creates a static _overlay_ to use it with google maps.
```js
var torqueLayer = new torque.GMapsTiledTorqueLayer({
provider: 'url_template',
url: GBIF_URL,
resolution: 4,
});
torqueLayer.setMap(map);
torqueLayer.setKey([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
```
see ``L.TiledTorqueLayer`` for options reference

View File

@ -0,0 +1,108 @@
<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="title">Average temperature collected by Britain's Royal Navy (1913-1925)</div>
<script src="../vendor/leaflet.js"></script>
<script src="../vendor/underscore.js"></script>
<script src="../vendor/carto.js"></script>
<script src="../dist/torque.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: "date";',
'-torque-aggregation-function: "avg(temp::float)";',
'-torque-frame-count: 1;',
'-torque-animation-duration: 15;',
'-torque-resolution: 16',
'}',
'#layer {',
' marker-width: 8;',
' marker-fill-opacity: 1.0;',
' marker-fill: #fff5eb; ',
' marker-type: rectangle;',
' [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, 0],
zoom: 3
});
L.tileLayer('http://{s}.api.cartocdn.com/base-dark/{z}/{x}/{y}.png', {
attribution: 'CartoDB'
}).addTo(map);
var torqueLayer = new L.TorqueLayer({
user : 'viz2',
table : 'ow',
cartocss: CARTOCSS
});
torqueLayer.addTo(map);
map.on('click', function(e) {
var p = e.containerPoint
var value = torqueLayer.getValueForPos(p.x, p.y);
if (value !== null) {
map.openPopup('average temperature: ' + value.value + "C", e.latlng);
}
});
// show small rectable and change cursor on hover
var hover = null;
map.on('mousemove', function(e) {
var p = e.containerPoint
var value = torqueLayer.getValueForPos(p.x, p.y);
// remove previous hover box
if (hover) {
map.removeLayer(hover);
hover = null;
}
if (value !== null) {
hover = L.rectangle(value.bbox, {
color: '#000',
weight: 1
}).addTo(map);
map._container.style.cursor = 'pointer';
} else {
map._container.style.cursor = 'auto';
}
});
</script>
</body>
</html>

View File

@ -330,6 +330,27 @@ L.TorqueLayer = L.CanvasLayer.extend({
positions = positions.concat(this.renderer.getActivePointsBBox(tile, step));
}
return positions;
},
/**
* return the value for position relative to map coordinates. null for no value
*/
getValueForPos: function(x, y, step) {
step = step === undefined ? this.key: step;
var t, tile, pos, value = null, xx, yy;
for(t in this._tiles) {
tile = this._tiles[t];
pos = this.getTilePos(tile.coord);
xx = x - pos.x;
yy = y - pos.y;
if (xx >= 0 && yy >= 0 && xx < this.renderer.TILE_SIZE && yy <= this.renderer.TILE_SIZE) {
value = this.renderer.getValueFor(tile, step, xx, yy);
}
if (value !== null) {
return value;
}
}
return null;
}
});

View File

@ -59,9 +59,10 @@ MercatorProjection.prototype._tilePixelPos = function(tileX, tileY) {
};
};
MercatorProjection.prototype.tilePixelBBox = function(x, y, zoom, px, py) {
MercatorProjection.prototype.tilePixelBBox = function(x, y, zoom, px, py, res) {
res = res || 1.0;
var numTiles = 1 <<zoom;
var inc = 1.0/numTiles;
var inc = res/numTiles;
px = (x*this._tileSize + px)/numTiles;
py = (y*this._tileSize + py)/numTiles;
return [

View File

@ -187,6 +187,41 @@
}
}
return positions;
},
// return the value for x, y (tile coordinates)
// null for no value
getValueFor: function(tile, step, px, py) {
var mercator = new torque.Mercator();
var res = this.options.resolution;
var res2 = res >> 1;
var tileMax = this.options.resolution * (this.TILE_SIZE/this.options.resolution - 1);
//this.renderer.renderTile(tile, this.key, pos.x, pos.y);
var activePixels = tile.timeCount[step];
var pixelIndex = tile.timeIndex[step];
for(var p = 0; p < activePixels; ++p) {
var posIdx = tile.renderDataPos[pixelIndex + p];
var c = tile.renderData[pixelIndex + p];
if (c) {
var x = tile.x[posIdx];
var y = tileMax - tile.y[posIdx];
var dx = px + res2 - x;
var dy = py + res2 - y;
if (dx >= 0 && dx < res && dy >= 0 && dy < res) {
return {
value: c,
bbox: mercator.tilePixelBBox(
tile.coord.x,
tile.coord.y,
tile.coord.z,
x - res2, y - res2, res
)
}
}
}
}
return null;
}
};

View File

@ -47,3 +47,31 @@ test('render conditional point layers', function() {
st = layer.getStyle('canvas-2d', {}, { zoom: 18, 'frame-offset': 0 });
equal(st['point-radius'], 20);
});
test('get value for position', function() {
var mercator = new torque.Mercator();
tile = {
timeCount: [1],
timeIndex: [0],
renderDataPos: [0],
renderData: [5],
x: [100],
y: [3],
coord: { x: 0, y: 0, z: 0 }
};
renderer.options = {
resolution: 1
};
var v = renderer.getValueFor(tile, 0, 100, 255 - 3);
var bbox = mercator.tilePixelBBox(0, 0, 0, 100, 255 - 3, 1);
equal(v.bbox[0].lat, bbox[0].lat);
equal(v.bbox[1].lat, bbox[1].lat);
equal(v.bbox[0].lon, bbox[0].lon);
equal(v.bbox[1].lon, bbox[1].lon);
equal(v.value, 5);
v = renderer.getValueFor(tile, 0, 100, 255 - 4);
equal(v, null);
v = renderer.getValueFor(tile, 0, 99, 255 - 3);
equal(v, null);
});

View File

@ -16,6 +16,7 @@
<script src="../lib/torque/core.js"></script>
<script src="../lib/torque/profiler.js"></script>
<script src="../lib/torque/request.js"></script>
<script src="../lib/torque/mercator.js"></script>
<script src="../lib/torque/leaflet/leaflet_tileloader_mixin.js"></script>
<script src="../lib/torque/leaflet/canvas_layer.js"></script>
<script src="../lib/torque/renderer/point.js"></script>