Skip to content

Commit

Permalink
Add queryObjects api to DeckGL component (visgl#673)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Jun 2, 2017
1 parent faa3ff9 commit 4dcdb65
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 58 deletions.
35 changes: 35 additions & 0 deletions docs/api-reference/deckgl.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,40 @@ object for the topmost picked layer at the coordinate
are affected.
- `event` - the original [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object

### Methods

##### `queryObject(options)`

Get the closest pickable and visible object at screen coordinate.

Parameters:
- `options` (Object)
+ `x` (Number) - x position in pixels
+ `y` (Number) - y position in pixels
+ `radius` (Number, optional) - radius of tolerance in pixels. Default `0`.
+ `layerIds` (Array, optional) - a list of layer ids to query from.
If not specified, then all pickable and visible layers are queried.

Returns: a single [`info`](/docs/get-started/interactivity.md#the-picking-info-object) object, or `null` if nothing is found.

##### `queryObjects(options)`

Get all pickable and visible objects within a bounding box.

Parameters:
- `options` (Object)
+ `x` (Number) - left of the bounding box in pixels
+ `y` (Number) - top of the bouding box in pixels
+ `width` (Number, optional) - width of the bouding box in pixels. Default `1`.
+ `height` (Number, optional) - height of the bouding box in pixels. Default `1`.
+ `layerIds` (Array, optional) - a list of layer ids to query from.
If not specified, then all pickable and visible layers are queried.

Returns: an array of unique [`info`](/docs/get-started/interactivity.md#the-picking-info-object) objects

Remarks:
- This query methods are designed to quickly find objects by utilizing the picking buffer. They offer more flexibility for developers to handle events in addition to the built-in hover and click callbacks.
- Note there is a limitation in the query methods: occluded objects are not returned. To improve the results, you may try setting the `layerIds` parameter to limit the query to fewer layers.

## Source
[src/react/deckgl.js](https://github.com/uber/deck.gl/blob/4.0-release/src/react/deckgl.js)
4 changes: 4 additions & 0 deletions docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

* `CompositeLayer.renderLayers` can now return a nested arrays with `null` values. deck.gl will automatically flatten and filter the array. This is a small convenience that makes the `renderLayers methods in complex composite layers a little more readable.

## Query Methods

* `DeckGL.queryObject` and `DeckGL.queryObjects` allow developers to directly query the picking buffer in addition to the built-in click and hover callbacks.

# deck.gl v4.0

Release date: March 31, 2017
Expand Down
17 changes: 13 additions & 4 deletions examples/layer-browser/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class App extends PureComponent {
// rotationX: 0
},
hoveredItem: null,
clickedItem: null
clickedItem: null,
queriedItems: null
};

this._effects = [new ReflectionEffect()];
Expand Down Expand Up @@ -111,6 +112,13 @@ class App extends PureComponent {
this.setState({clickedItem: info});
}

_onQueryObjects() {
const {width, height} = this.state;
const infos = this.refs.deckgl.queryObjects({x: 0, y: 0, width, height});
console.log(infos); // eslint-disable-line
this.setState({queriedItems: infos});
}

_renderExampleLayer(example, settings, index) {
const {layer: Layer, props, getData} = example;
const layerProps = Object.assign({}, props, settings);
Expand Down Expand Up @@ -185,7 +193,7 @@ class App extends PureComponent {
{ ...mapViewState }
onChangeViewport={this._onViewportChanged}>

<DeckGL
<DeckGL ref="deckgl"
debug
id="default-deckgl-overlay"
width={width} height={height}
Expand Down Expand Up @@ -215,13 +223,14 @@ class App extends PureComponent {
}

render() {
const {settings, activeExamples, hoveredItem, clickedItem} = this.state;
const {settings, activeExamples, hoveredItem, clickedItem, queriedItems} = this.state;

return (
<div>
{ this._renderMap() }
{ !MAPBOX_ACCESS_TOKEN && this._renderNoTokenWarning() }
<div id="control-panel">
<button onClick={this._onQueryObjects}>Query Objects</button>
<LayerControls
title="Composite Settings"
settings={settings}
Expand All @@ -232,7 +241,7 @@ class App extends PureComponent {
onToggleLayer={this._onToggleLayer}
onUpdateLayer={this._onUpdateLayerSettings} />
</div>
<LayerInfo ref="infoPanel" hovered={hoveredItem} clicked={clickedItem} />
<LayerInfo ref="infoPanel" hovered={hoveredItem} clicked={clickedItem} queried={queriedItems} />
</div>
);
}
Expand Down
6 changes: 5 additions & 1 deletion examples/layer-browser/src/components/layer-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class LayerInfo extends PureComponent {
}

render() {
const {hovered, clicked} = this.props;
const {hovered, clicked, queried} = this.props;

return (
<div id="layer-info">
Expand All @@ -24,6 +24,10 @@ export default class LayerInfo extends PureComponent {
<h4>Click</h4>
<span>Layer: { clicked.layer.id } Object: { this._infoToString(clicked) }</span>
</div>) }
{ queried && (<div>
<h4>Query</h4>
<span>{ queried.length } Objects found</span>
</div>) }
</div>
);
}
Expand Down
175 changes: 137 additions & 38 deletions src/lib/draw-and-pick.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,60 @@ export function drawLayers({layers, pass}) {
${visibleCount} visible, ${layers.length} total`);
}

// Pick all objects within the given bounding box
export function queryLayers(gl, {
layers,
pickingFBO,
x,
y,
width,
height,
viewport,
mode
}) {

// Convert from canvas top-left to WebGL bottom-left coordinates
// And compensate for pixelRatio
const pixelRatio = typeof window !== 'undefined' ?
window.devicePixelRatio : 1;
const deviceLeft = Math.round(x * pixelRatio);
const deviceBottom = Math.round(gl.canvas.height - y * pixelRatio);
const deviceRight = Math.round((x + width) * pixelRatio);
const deviceTop = Math.round(gl.canvas.height - (y + height) * pixelRatio);

const pickInfos = getUniquesFromPickingBuffer(gl, {
layers,
pickingFBO,
deviceRect: {
x: deviceLeft,
y: deviceTop,
width: deviceRight - deviceLeft,
height: deviceBottom - deviceTop
}
});

// Only return unique infos, identified by info.object
const uniqueInfos = new Map();

pickInfos.forEach(pickInfo => {
let info = createInfo([pickInfo.x / pixelRatio, pickInfo.y / pixelRatio], viewport);
info.devicePixel = [pickInfo.x, pickInfo.y];
info.pixelRatio = pixelRatio;
info.color = pickInfo.pickedColor;
info.index = pickInfo.pickedObjectIndex;
info.picked = true;

info = getLayerPickingInfo({layer: pickInfo.pickedLayer, info, mode});
if (!uniqueInfos.has(info.object)) {
uniqueInfos.set(info.object, info);
}
});

return Array.from(uniqueInfos.values());
}

/* eslint-disable max-depth, max-statements */
// Pick the closest object at the given (x,y) coordinate
export function pickLayers(gl, {
layers,
pickingFBO,
Expand Down Expand Up @@ -96,7 +149,7 @@ export function pickLayers(gl, {

} else {
// For all other events, run picking process normally.
const pickInfo = pickFromBuffer(gl, {
const pickInfo = getClosestFromPickingBuffer(gl, {
layers,
pickingFBO,
deviceX,
Expand Down Expand Up @@ -183,6 +236,7 @@ export function pickLayers(gl, {
case 'dragend': handled = info.layer.props.onDragEnd(info); break;
case 'dragcancel': handled = info.layer.props.onDragCancel(info); break;
case 'hover': handled = info.layer.props.onHover(info); break;
case 'query': break;
default: throw new Error('unknown pick type');
}

Expand All @@ -198,7 +252,7 @@ export function pickLayers(gl, {
* Pick at a specified pixel with a tolerance radius
* Returns the closest object to the pixel in shape `{pickedColor, pickedLayer, pickedObjectIndex}`
*/
function pickFromBuffer(gl, {
function getClosestFromPickingBuffer(gl, {
layers,
pickingFBO,
deviceX,
Expand All @@ -212,6 +266,86 @@ function pickFromBuffer(gl, {
const width = Math.min(pickingFBO.width, deviceX + deviceRadius) - x + 1;
const height = Math.min(pickingFBO.height, deviceY + deviceRadius) - y + 1;

const pickedColors = getPickedColors(gl, {layers, pickingFBO, deviceRect: {x, y, width, height}});

// Traverse all pixels in picking results and find the one closest to the supplied
// [deviceX, deviceY]
let minSquareDistanceToCenter = deviceRadius * deviceRadius;
let closestResultToCenter = {
pickedColor: EMPTY_PIXEL,
pickedLayer: null,
pickedObjectIndex: -1
};
let i = 0;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
// Decode picked layer from color
const pickedLayerIndex = pickedColors[i + 3] - 1;

if (pickedLayerIndex >= 0) {
const dx = col + x - deviceX;
const dy = row + y - deviceY;
const d2 = dx * dx + dy * dy;

if (d2 <= minSquareDistanceToCenter) {
minSquareDistanceToCenter = d2;

// Decode picked object index from color
const pickedColor = pickedColors.slice(i, i + 4);
const pickedLayer = layers[pickedLayerIndex];
const pickedObjectIndex = pickedLayer.decodePickingColor(pickedColor);
closestResultToCenter = {pickedColor, pickedLayer, pickedObjectIndex};
}
}
i += 4;
}
}

return closestResultToCenter;
}
/* eslint-enable max-depth, max-statements */

/**
* Query within a specified rectangle
* Returns array of unique objects in shape `{x, y, pickedColor, pickedLayer, pickedObjectIndex}`
*/
function getUniquesFromPickingBuffer(gl, {
layers,
pickingFBO,
deviceRect: {x, y, width, height}
}) {
const pickedColors = getPickedColors(gl, {layers, pickingFBO, deviceRect: {x, y, width, height}});
const uniqueColors = new Map();

// Traverse all pixels in picking results and get unique colors
for (let i = 0; i < pickedColors.length; i += 4) {
// Decode picked layer from color
const pickedLayerIndex = pickedColors[i + 3] - 1;

if (pickedLayerIndex >= 0) {
const pickedColor = pickedColors.slice(i, i + 4);
const colorKey = pickedColor.join(',');
if (!uniqueColors.has(colorKey)) {
const pickedLayer = layers[pickedLayerIndex];
uniqueColors.set(colorKey, {
pickedColor,
pickedLayer,
pickedObjectIndex: pickedLayer.decodePickingColor(pickedColor)
});
}
}
}

return Array.from(uniqueColors.values());
}

// Returns an Uint8ClampedArray of picked pixels
function getPickedColors(gl, {
layers,
pickingFBO,
deviceRect: {x, y, width, height}
}) {
// TODO - just return glContextWithState once luma updates
// Make sure we clear scissor test and fbo bindings in case of exceptions
// We are only interested in one pixel, no need to render anything else
Expand Down Expand Up @@ -257,44 +391,9 @@ function pickFromBuffer(gl, {
// restore blend mode
setBlendMode(gl, oldBlendMode);

// Traverse all pixels in picking results and find the one closest to the supplied
// [deviceX, deviceY]
let minSquareDistanceToCenter = deviceRadius * deviceRadius;
let closestResultToCenter = null;
let i = 0;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
// Decode picked layer from color
const pickedLayerIndex = pickedColors[i + 3] - 1;

if (pickedLayerIndex >= 0) {
const dx = col + x - deviceX;
const dy = row + y - deviceY;
const d2 = dx * dx + dy * dy;

if (d2 <= minSquareDistanceToCenter) {
minSquareDistanceToCenter = d2;

// Decode picked object index from color
const pickedColor = pickedColors.slice(i, i + 4);
const pickedLayer = layers[pickedLayerIndex];
const pickedObjectIndex = pickedLayer.decodePickingColor(pickedColor);
closestResultToCenter = {pickedColor, pickedLayer, pickedObjectIndex};
}
}
i += 4;
}
}

return closestResultToCenter || {
pickedColor: EMPTY_PIXEL,
pickedLayer: null,
pickedObjectIndex: -1
};
return pickedColors;
});
}
/* eslint-enable max-depth, max-statements */

function createInfo(pixel, viewport) {
// Assign a number of potentially useful props to the "info" object
Expand Down
Loading

0 comments on commit 4dcdb65

Please sign in to comment.