Skip to content

Commit

Permalink
add picking by bounding box
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoji Chen committed May 12, 2017
1 parent 3562346 commit 571a8df
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 46 deletions.
11 changes: 11 additions & 0 deletions examples/layer-browser/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ class App extends PureComponent {
setImmutableDataSamples(settings.immutable);
}

_onQueryFeatures() {
const {deckgl} = this.refs;
const {width, height} = this.state;
console.log(deckgl.elementsInScope([0, 0], [width, height])); // eslint-disable-line
}

_onHover(info) {
this.setState({hoveredItem: info});
}
Expand Down Expand Up @@ -186,6 +192,7 @@ class App extends PureComponent {

<DeckGL
debug
ref="deckgl"
id="default-deckgl-overlay"
width={width} height={height}
{...mapViewState}
Expand Down Expand Up @@ -220,6 +227,10 @@ class App extends PureComponent {
{ this._renderMap() }
{ !MAPBOX_ACCESS_TOKEN && this._renderNoTokenWarning() }
<div id="control-panel">
<div>
<h4 />
<button onClick={this._onQueryFeatures} >Query Rendered Features</button>
</div>
<LayerControls
title="Composite Settings"
settings={settings}
Expand Down
204 changes: 167 additions & 37 deletions src/lib/draw-and-pick.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export function drawLayers({layers, pass}) {
export function pickLayers(gl, {
layers,
pickingFBO,
uniforms = {},
x,
y,
radius,
viewport,
mode,
lastPickedInfo
Expand All @@ -71,8 +71,9 @@ export function pickLayers(gl, {
// And compensate for pixelRatio
const pixelRatio = typeof window !== 'undefined' ?
window.devicePixelRatio : 1;
const deviceX = x * pixelRatio;
const deviceY = gl.canvas.height - y * pixelRatio;
const deviceX = Math.round(x * pixelRatio);
const deviceY = Math.round(gl.canvas.height - y * pixelRatio);
const deviceRadius = Math.round(radius * pixelRatio);

let pickedColor;
let pickedLayer;
Expand All @@ -95,16 +96,18 @@ export function pickLayers(gl, {

} else {
// For all other events, run picking process normally.
const pickInfo = pickFromBuffer(gl, {
const pickResult = pickFromBuffer(gl, {
layers,
pickingFBO,
deviceX,
deviceY
deviceY,
deviceRadius
});
pickedColor = pickInfo.pickedColor;
pickedLayer = pickInfo.pickedLayer;
pickedObjectIndex = pickInfo.pickedObjectIndex;
affectedLayers = pickInfo.affectedLayers;

pickedColor = pickResult.color;
pickedLayer = pickResult.layer;
pickedObjectIndex = pickResult.objectIndex;
affectedLayers = pickedLayer ? [pickedLayer] : [];

if (mode === 'hover') {
// only invoke onHover events if picked object has changed
Expand Down Expand Up @@ -152,21 +155,7 @@ export function pickLayers(gl, {
info.picked = true;
}

// Walk up the composite chain and find the owner of the event
// sublayers are never directly exposed to the user
while (layer && info) {
// For a composite layer, sourceLayer will point to the sublayer
// where the event originates from.
// It provides additional context for the composite layer's
// getPickingInfo() method to populate the info object
const sourceLayer = info.layer || layer;
info.layer = layer;
// layer.pickLayer() function requires a non-null ```layer.state```
// object to funtion properly. So the layer refereced here
// must be the "current" layer, not an "out-dated" / "invalidated" layer
info = layer.pickLayer({info, mode, sourceLayer});
layer = layer.parentLayer;
}
info = getLayerPickingInfo({layer, info, mode});

// This guarantees that there will be only one copy of info for
// one composite layer
Expand Down Expand Up @@ -206,11 +195,122 @@ export function pickLayers(gl, {
}
/* eslint-enable max-depth, max-statements */

export function pickLayersByBoundingBox(gl, {
layers,
pickingFBO,
boundingBox: {x, y, width, height},
viewport
}) {

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

// For all other events, run picking process normally.
const pickResults = pickFromBufferByBoundingBox(gl, {
layers,
pickingFBO,
deviceBox
});

const baseInfo = createInfo([x, y], viewport);
baseInfo.pixelRatio = pixelRatio;

// Convert each result to pickingInfo object
const infos = {};

pickResults.filter(Boolean)
.forEach(({color, layer, objectIndex}) => {
// Get unique objects
const objectId = `${layer.id}:${objectIndex}`;

if (!infos[objectId]) {
const info = Object.assign({}, baseInfo, {
color,
index: objectIndex,
picked: true
});

infos[objectId] = getLayerPickingInfo({
layer,
info,
mode: 'query'
});
}
});

return Object.values(infos);
}

/**
* Pick at a specified pixel with a tolerance radius
* Returns the closest object to the pixel in shape `{color, layer, objectIndex}`
*/
function pickFromBuffer(gl, {
layers,
pickingFBO,
deviceX,
deviceY
deviceY,
deviceRadius
}) {

// Create a box of size `radius * 2 + 1` centered at [deviceX, deviceY]
const x = Math.max(0, deviceX - deviceRadius);
const y = Math.max(0, deviceY - deviceRadius);
const deviceBox = {
x,
y,
width: Math.min(pickingFBO.width, deviceX + deviceRadius) - x + 1,
height: Math.min(pickingFBO.height, deviceY + deviceRadius) - y + 1
};

const pickResults = pickFromBufferByBoundingBox(gl, {layers, pickingFBO, deviceBox});

// Traverse all pixels in picking results and find the one closest to the supplied
// [deviceX, deviceY]
let minDistanceToCenter = deviceRadius;
let closestResultToCenter = null;
/* eslint-disable max-depth */
for (let col = 0; col < deviceBox.width; col++) {
for (let row = 0; row < deviceBox.height; row++) {
const pickResult = pickResults[row * deviceBox.width + col];
if (pickResult) {
const dx = deviceBox.x + col - deviceX;
const dy = deviceBox.y + row - deviceY;
const d = Math.sqrt(dx * dx + dy * dy);
if (d <= minDistanceToCenter) {
minDistanceToCenter = d;
closestResultToCenter = pickResult;
pickResult.x = deviceBox.x + col;
pickResult.y = deviceBox.y + row;
}
}
}
}
/* eslint-enable max-depth */

return closestResultToCenter || {
color: EMPTY_PIXEL,
layer: null,
objectIndex: -1
};
}

/**
* Returns an array of picked object for each pixel in bounding box
* In the shape of `{color, layer, objectIndex}`
*/
function pickFromBufferByBoundingBox(gl, {
layers,
pickingFBO,
deviceBox: {x, y, width, height}
}) {
// TODO - just return glContextWithState once luma updates
// Make sure we clear scissor test and fbo bindings in case of exceptions
Expand All @@ -219,7 +319,7 @@ function pickFromBuffer(gl, {
return glContextWithState(gl, {
frameBuffer: pickingFBO,
framebuffer: pickingFBO,
scissorTest: {x: deviceX, y: deviceY, w: 1, h: 1}
scissorTest: {x, y, w: width, h: height}
}, () => {

// Clear the frame buffer
Expand Down Expand Up @@ -251,21 +351,33 @@ function pickFromBuffer(gl, {
});

// Read color in the central pixel, to be mapped with picking colors
const pickedColor = new Uint8Array(4);
gl.readPixels(deviceX, deviceY, 1, 1, GL.RGBA, GL.UNSIGNED_BYTE, pickedColor);
const pickedColors = new Uint8Array(width * height * 4);
gl.readPixels(x, y, width, height, GL.RGBA, GL.UNSIGNED_BYTE, pickedColors);

// restore blend mode
setBlendMode(gl, oldBlendMode);

// Decode picked color
const pickedLayerIndex = pickedColor[3] - 1;
const pickedLayer = pickedLayerIndex >= 0 ? layers[pickedLayerIndex] : null;
return {
pickedColor,
pickedLayer,
pickedObjectIndex: pickedLayer ? pickedLayer.decodePickingColor(pickedColor) : -1,
affectedLayers: pickedLayer ? [pickedLayer] : []
};
const pickResults = [];

for (let i = 0; i < pickedColors.length; i += 4) {
// Decode picked color
const pickedLayerIndex = pickedColors[i + 3] - 1;
if (pickedLayerIndex >= 0) {
const pickedLayer = layers[pickedLayerIndex];
const pickedColor = pickedColors.slice(i, i + 4);
const pickedObjectIndex = pickedLayer.decodePickingColor(pickedColor);

pickResults.push({
color: pickedColor,
layer: pickedLayer,
objectIndex: pickedObjectIndex
});
} else {
pickResults.push(null);
}
}

return pickResults;
});
}

Expand All @@ -281,3 +393,21 @@ function createInfo(pixel, viewport) {
lngLat: viewport.unproject(pixel)
};
}

// Walk up the layer composite chain to populate the info object
function getLayerPickingInfo({layer, info, mode}) {
while (layer && info) {
// For a composite layer, sourceLayer will point to the sublayer
// where the event originates from.
// It provides additional context for the composite layer's
// getPickingInfo() method to populate the info object
const sourceLayer = info.layer || layer;
info.layer = layer;
// layer.pickLayer() function requires a non-null ```layer.state```
// object to funtion properly. So the layer refereced here
// must be the "current" layer, not an "out-dated" / "invalidated" layer
info = layer.pickLayer({info, mode, sourceLayer});
layer = layer.parentLayer;
}
return info;
}
29 changes: 23 additions & 6 deletions src/lib/layer-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import Layer from './layer';
import {log} from './utils';
import assert from 'assert';
import {drawLayers, pickLayers} from './draw-and-pick';
import {drawLayers, pickLayers, pickLayersByBoundingBox} from './draw-and-pick';
import {LIFECYCLE} from './constants';
import {Viewport} from './viewports';
import {setOverride, layerEditListener, logLayer} from '../debug/seer-integration';
Expand Down Expand Up @@ -146,7 +146,7 @@ export default class LayerManager {
return this;
}

pickLayer({x, y, mode}) {
pickLayer({x, y, mode, radius = 0}) {
const {gl, uniforms} = this.context;

// Set up a frame buffer if needed
Expand All @@ -161,10 +161,7 @@ export default class LayerManager {
return pickLayers(gl, {
x,
y,
uniforms: {
renderPickingBuffer: true,
picking_uEnable: true
},
radius,
layers: this.layers,
mode,
viewport: this.context.viewport,
Expand All @@ -173,6 +170,26 @@ export default class LayerManager {
});
}

pickLayerByBoundingBox({x, y, width, height}) {
const {gl, uniforms} = this.context;

// Set up a frame buffer if needed
if (this.context.pickingFBO === null ||
gl.canvas.width !== this.context.pickingFBO.width ||
gl.canvas.height !== this.context.pickingFBO.height) {
this.context.pickingFBO = new FramebufferObject(gl, {
width: gl.canvas.width,
height: gl.canvas.height
});
}
return pickLayersByBoundingBox(gl, {
layers: this.layers,
viewport: this.context.viewport,
pickingFBO: this.context.pickingFBO,
boundingBox: {x, y, width, height}
});
}

needsRedraw({clearRedrawFlags = false} = {}) {
if (!this.context.viewport) {
return false;
Expand Down

0 comments on commit 571a8df

Please sign in to comment.