From 78466684dc1f50bf978dd26d92470750067f556c Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 2 Jul 2019 15:14:19 -0700 Subject: [PATCH 1/4] add brushing extension --- examples/website/brushing/app.js | 44 +++++----- .../brushing-layers/arc-brushing-layer.js | 72 ---------------- .../scatterplot-brushing-layer.js | 86 ------------------- modules/extensions/src/brushing/brushing.js | 70 +++++++++++++++ .../extensions/src/brushing/shader-module.js | 70 +++++++++++---- modules/extensions/src/index.js | 1 + 6 files changed, 148 insertions(+), 195 deletions(-) delete mode 100644 examples/website/brushing/brushing-layers/arc-brushing-layer.js delete mode 100644 examples/website/brushing/brushing-layers/scatterplot-brushing-layer.js create mode 100644 modules/extensions/src/brushing/brushing.js rename examples/website/brushing/brushing-layers/brushing-shader-module.js => modules/extensions/src/brushing/shader-module.js (62%) diff --git a/examples/website/brushing/app.js b/examples/website/brushing/app.js index c825a64f020..2f2d85c42d2 100644 --- a/examples/website/brushing/app.js +++ b/examples/website/brushing/app.js @@ -2,9 +2,9 @@ import React, {Component} from 'react'; import {render} from 'react-dom'; import {StaticMap} from 'react-map-gl'; -import DeckGL from 'deck.gl'; -import ArcBrushingLayer from './brushing-layers/arc-brushing-layer'; -import ScatterplotBrushingLayer from './brushing-layers/scatterplot-brushing-layer'; +import DeckGL from '@deck.gl/react'; +import {ScatterplotLayer, ArcLayer} from '@deck.gl/layers'; +import {BrushingExtension} from '@deck.gl/extensions'; import {scaleLinear} from 'd3-scale'; // Set your mapbox token here @@ -42,6 +42,8 @@ const INITIAL_VIEW_STATE = { bearing: 0 }; +const brushingExtension = new BrushingExtension(); + /* eslint-disable react/no-deprecated */ export class App extends Component { constructor(props) { @@ -182,58 +184,60 @@ export class App extends Component { } return [ - new ScatterplotBrushingLayer({ + new ScatterplotLayer({ id: 'sources', data: sources, - brushRadius, - brushTarget: true, + brushingRadius: brushRadius, mousePosition, opacity: 1, - enableBrushing: startBrushing, + brushingEnabled: startBrushing, pickable: false, // only show source points when brushing radiusScale: startBrushing ? 3000 : 0, getFillColor: d => (d.gain > 0 ? TARGET_COLOR : SOURCE_COLOR), - getTargetPosition: d => [d.position[0], d.position[1], 0] + extensions: [brushingExtension] }), - new ScatterplotBrushingLayer({ + new ScatterplotLayer({ id: 'targets-ring', data: targets, - brushRadius, + brushingRadius: brushRadius, mousePosition, lineWidthMinPixels: 2, stroked: true, filled: false, opacity: 1, - enableBrushing: startBrushing, + brushingEnabled: startBrushing, // only show rings when brushing radiusScale: startBrushing ? 4000 : 0, - getLineColor: d => (d.net > 0 ? TARGET_COLOR : SOURCE_COLOR) + getLineColor: d => (d.net > 0 ? TARGET_COLOR : SOURCE_COLOR), + extensions: [brushingExtension] }), - new ScatterplotBrushingLayer({ + new ScatterplotLayer({ id: 'targets', data: targets, - brushRadius, + brushingRadius: brushRadius, mousePosition, opacity: 1, - enableBrushing: startBrushing, + brushingEnabled: startBrushing, pickable: true, radiusScale: 3000, onHover: this._onHover, - getFillColor: d => (d.net > 0 ? TARGET_COLOR : SOURCE_COLOR) + getFillColor: d => (d.net > 0 ? TARGET_COLOR : SOURCE_COLOR), + extensions: [brushingExtension] }), - new ArcBrushingLayer({ + new ArcLayer({ id: 'arc', data: arcs, getWidth: strokeWidth, opacity, - brushRadius, - enableBrushing: startBrushing, + brushingRadius: brushRadius, + brushingEnabled: startBrushing, mousePosition, getSourcePosition: d => d.source, getTargetPosition: d => d.target, getSourceColor: SOURCE_COLOR, - getTargetColor: TARGET_COLOR + getTargetColor: TARGET_COLOR, + extensions: [brushingExtension] }) ]; } diff --git a/examples/website/brushing/brushing-layers/arc-brushing-layer.js b/examples/website/brushing/brushing-layers/arc-brushing-layer.js deleted file mode 100644 index 57ec36017f2..00000000000 --- a/examples/website/brushing/brushing-layers/arc-brushing-layer.js +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2015-2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import {ArcLayer} from '@deck.gl/layers'; -import brushingShaderModule from './brushing-shader-module'; - -const defaultProps = { - // show arc if source is in brush - brushSource: true, - // show arc if target is in brush - brushTarget: true, - enableBrushing: true, - // brush radius in meters - brushRadius: 100000, - mousePosition: null -}; - -export default class ArcBrushingLayer extends ArcLayer { - getShaders() { - const shaders = super.getShaders(); - - shaders.modules.push(brushingShaderModule); - - shaders.inject = { - 'vs:#decl': ` -uniform bool brushSource; -uniform bool brushTarget; -`, - 'vs:#main-end': ` - brushing_setVisible( - (brushSource && brushing_isPointInRange(instancePositions.xy)) || - (brushTarget && brushing_isPointInRange(instancePositions.zw)) - ); -`, - 'fs:#main-end': ` - gl_FragColor = brushing_filterBrushingColor(gl_FragColor); -` - }; - - return shaders; - } - - draw(opts) { - // add uniforms - const uniforms = Object.assign({}, opts.uniforms, { - brushSource: this.props.brushSource, - brushTarget: this.props.brushTarget - }); - const newOpts = Object.assign({}, opts, {uniforms}); - super.draw(newOpts); - } -} - -ArcBrushingLayer.layerName = 'ArcBrushingLayer'; -ArcBrushingLayer.defaultProps = defaultProps; diff --git a/examples/website/brushing/brushing-layers/scatterplot-brushing-layer.js b/examples/website/brushing/brushing-layers/scatterplot-brushing-layer.js deleted file mode 100644 index d5c14767708..00000000000 --- a/examples/website/brushing/brushing-layers/scatterplot-brushing-layer.js +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2015-2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import {ScatterplotLayer} from '@deck.gl/layers'; -import brushingShaderModule from './brushing-shader-module'; - -const defaultProps = { - enableBrushing: true, - // show point only if target is in brush - brushTarget: false, - // brush radius in meters - brushRadius: 100000, - mousePosition: null, - getTargetPosition: d => d.target, - radiusMinPixels: 0 -}; - -export default class ScatterplotBrushingLayer extends ScatterplotLayer { - getShaders() { - const shaders = super.getShaders(); - - shaders.modules.push(brushingShaderModule); - - shaders.inject = { - 'vs:#decl': ` -attribute vec3 instanceTargetPositions; - -uniform bool brushTarget; -`, - 'vs:#main-end': ` - brushing_setVisible(brushTarget? - brushing_isPointInRange(instanceTargetPositions.xy) : - brushing_isPointInRange(instancePositions.xy) - ); -`, - 'fs:#main-end': ` - gl_FragColor = brushing_filterBrushingColor(gl_FragColor); -` - }; - - return shaders; - } - - // add instanceSourcePositions as attribute - // instanceSourcePositions is used to calculate whether - // point source is in range when brushTarget is truthy - initializeState() { - super.initializeState(); - - this.state.attributeManager.addInstanced({ - instanceTargetPositions: { - size: 3, - accessor: 'getTargetPosition' - } - }); - } - - draw(opts) { - // add uniforms - const uniforms = Object.assign({}, opts.uniforms, { - brushTarget: this.props.brushTarget - }); - const newOpts = Object.assign({}, opts, {uniforms}); - super.draw(newOpts); - } -} - -ScatterplotBrushingLayer.layerName = 'ScatterplotBrushingLayer'; -ScatterplotBrushingLayer.defaultProps = defaultProps; diff --git a/modules/extensions/src/brushing/brushing.js b/modules/extensions/src/brushing/brushing.js new file mode 100644 index 00000000000..12f045a90dd --- /dev/null +++ b/modules/extensions/src/brushing/brushing.js @@ -0,0 +1,70 @@ +// Copyright (c) 2015 - 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import {LayerExtension} from '@deck.gl/core'; +import shaderModule from './shader-module'; + +const defaultProps = { + getBrushingTarget: {type: 'accessor', value: [0, 0]}, + + mousePosition: null, + + brushingTarget: 'source', + brushingEnabled: true, + brushingRadius: 10000 +}; + +export default class BrushingExtension extends LayerExtension { + getShaders(extension) { + return { + modules: [shaderModule] + }; + } + + initializeState(context, extension) { + const attributeManager = this.getAttributeManager(); + if (attributeManager) { + attributeManager.add({ + brushingTargets: { + size: 2, + accessor: 'getBrushingTarget', + update: !this.props.getBrushingTarget && extension.useConstantTargetPositions, + shaderAttributes: { + brushingTargets: { + divisor: 0 + }, + instanceBrushingTargets: { + divisor: 1 + } + } + } + }); + } + } + + useConstantTargetPositions(attribute) { + attribute.constant = true; + attribute.value = new Float32Array(2); + return; + } +} + +BrushingExtension.extensionName = 'BrushingExtension'; +BrushingExtension.defaultProps = defaultProps; diff --git a/examples/website/brushing/brushing-layers/brushing-shader-module.js b/modules/extensions/src/brushing/shader-module.js similarity index 62% rename from examples/website/brushing/brushing-layers/brushing-shader-module.js rename to modules/extensions/src/brushing/shader-module.js index f871e03a960..3e47dbcbfd4 100644 --- a/examples/website/brushing/brushing-layers/brushing-shader-module.js +++ b/modules/extensions/src/brushing/shader-module.js @@ -17,15 +17,23 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import {createModuleInjection} from '@luma.gl/core'; const vs = ` const float R_EARTH = 6371000.; // earth radius in km uniform bool brushing_enabled; + uniform int brushing_target; uniform vec2 brushing_mousePos; uniform float brushing_radius; - varying float brushing_hidden; + #ifdef NON_INSTANCED_MODEL + attribute vec2 brushingTargets; + #else + attribute vec2 instanceBrushingTargets; + #endif + + varying float brushing_isVisible; // approximate distance between lng lat in meters float distanceBetweenLatLng(vec2 source, vec2 target) { @@ -49,35 +57,63 @@ const vs = ` } void brushing_setVisible(bool visible) { - brushing_hidden = float(!visible); + brushing_isVisible = float(visible); } `; const fs = ` uniform bool brushing_enabled; - - varying float brushing_hidden; - - vec4 brushing_filterBrushingColor(vec4 color) { - if (brushing_enabled && brushing_hidden > 0.5) { - discard; - } - return color; - } + varying float brushing_isVisible; `; -const INITIAL_MODULE_OPTIONS = {}; +// filter_setValue(instanceFilterValue); +const moduleName = 'brushing'; + +const TARGET = { + source: 0, + target: 1, + custom: 2 +}; + +createModuleInjection(moduleName, { + hook: 'vs:DECKGL_FILTER_GL_POSITION', + injection: ` +vec2 brushingTarget; +if (brushing_target == 0) { + brushingTarget = geometry.worldPosition.xy; +} else if (brushing_target == 1) { + brushingTarget = geometry.worldPositionAlt.xy; +} else { + #ifdef NON_INSTANCED_MODEL + brushingTarget = brushingTargets; + #else + brushingTarget = instanceBrushingTargets; + #endif +} +brushing_setVisible(brushing_isPointInRange(brushingTarget)); + ` +}); + +createModuleInjection(moduleName, { + hook: 'fs:DECKGL_FILTER_COLOR', + injection: ` +if (brushing_enabled && brushing_isVisible < 0.5) { + discard; +} + ` +}); export default { - name: 'brushing', + name: moduleName, dependencies: ['project'], vs, fs, - getUniforms: (opts = INITIAL_MODULE_OPTIONS) => { - if (opts.viewport) { + getUniforms: opts => { + if (opts && opts.viewport) { return { - brushing_enabled: opts.enableBrushing, - brushing_radius: opts.brushRadius, + brushing_enabled: opts.brushingEnabled, + brushing_radius: opts.brushingRadius, + brushing_target: TARGET[opts.brushingTarget] || 0, brushing_mousePos: opts.mousePosition ? opts.viewport.unproject(opts.mousePosition) : [0, 0] }; } diff --git a/modules/extensions/src/index.js b/modules/extensions/src/index.js index 25922c6c265..ef4b55b8db5 100644 --- a/modules/extensions/src/index.js +++ b/modules/extensions/src/index.js @@ -1 +1,2 @@ +export {default as BrushingExtension} from './brushing/brushing'; export {default as DataFilterExtension} from './data-filter/data-filter'; From 6d0f5cbd53a0c89566e87508850b3e17d0deeee9 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 2 Jul 2019 15:28:05 -0700 Subject: [PATCH 2/4] add tests --- modules/extensions/src/brushing/brushing.js | 2 + .../extensions/src/brushing/shader-module.js | 22 +++++---- test/modules/extensions/brushing.spec.js | 47 +++++++++++++++++++ test/modules/extensions/index.js | 1 + 4 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 test/modules/extensions/brushing.spec.js diff --git a/modules/extensions/src/brushing/brushing.js b/modules/extensions/src/brushing/brushing.js index 12f045a90dd..b742fc5dd0c 100644 --- a/modules/extensions/src/brushing/brushing.js +++ b/modules/extensions/src/brushing/brushing.js @@ -45,6 +45,8 @@ export default class BrushingExtension extends LayerExtension { brushingTargets: { size: 2, accessor: 'getBrushingTarget', + // Hack: extension's defaultProps is not merged with the layer's defaultProps, + // So we can't use the standard accessor when the prop is undefined update: !this.props.getBrushingTarget && extension.useConstantTargetPositions, shaderAttributes: { brushingTargets: { diff --git a/modules/extensions/src/brushing/shader-module.js b/modules/extensions/src/brushing/shader-module.js index 3e47dbcbfd4..666e87c9ca9 100644 --- a/modules/extensions/src/brushing/shader-module.js +++ b/modules/extensions/src/brushing/shader-module.js @@ -109,14 +109,20 @@ export default { vs, fs, getUniforms: opts => { - if (opts && opts.viewport) { - return { - brushing_enabled: opts.brushingEnabled, - brushing_radius: opts.brushingRadius, - brushing_target: TARGET[opts.brushingTarget] || 0, - brushing_mousePos: opts.mousePosition ? opts.viewport.unproject(opts.mousePosition) : [0, 0] - }; + if (!opts || !opts.viewport) { + return {}; } - return {}; + const { + brushingEnabled = true, + brushingRadius = 10000, + brushingTarget = 'source', + mousePosition = null + } = opts; + return { + brushing_enabled: Boolean(brushingEnabled && mousePosition), + brushing_radius: brushingRadius, + brushing_target: TARGET[brushingTarget] || 0, + brushing_mousePos: mousePosition ? opts.viewport.unproject(mousePosition) : [0, 0] + }; } }; diff --git a/test/modules/extensions/brushing.spec.js b/test/modules/extensions/brushing.spec.js new file mode 100644 index 00000000000..0ffdf0fc3fb --- /dev/null +++ b/test/modules/extensions/brushing.spec.js @@ -0,0 +1,47 @@ +import test from 'tape-catch'; +import {BrushingExtension} from '@deck.gl/extensions'; +import {ScatterplotLayer} from '@deck.gl/layers'; +import {testLayer} from '@deck.gl/test-utils'; + +test('BrushingExtension', t => { + const testCases = [ + { + props: { + data: [ + {position: [-122.453, 37.782], timestamp: 120, entry: 13567, exit: 4802}, + {position: [-122.454, 37.781], timestamp: 140, entry: 14475, exit: 5493} + ], + getPosition: d => d.position, + getBrushingTarget: d => d.position, + + extensions: [new BrushingExtension()] + }, + onAfterUpdate: ({layer}) => { + const {uniforms} = layer.state.model.program; + t.ok(uniforms.brushing_radius, 'has correct uniforms'); + t.is(uniforms.brushing_enabled, false, 'has correct uniforms'); + t.is(uniforms.brushing_target, 0, 'has correct uniforms'); + t.is(uniforms.brushing_mousePos[0], 0, 'has correct uniforms'); + } + }, + { + updateProps: { + brushingEnabled: true, + mousePosition: [1, 1], + brushingTarget: 'custom', + brushingRadius: 5e6 + }, + onAfterUpdate: ({layer}) => { + const {uniforms} = layer.state.model.program; + t.is(uniforms.brushing_radius, 5e6, 'has correct uniforms'); + t.is(uniforms.brushing_enabled, true, 'has correct uniforms'); + t.is(uniforms.brushing_target, 2, 'has correct uniforms'); + t.not(uniforms.brushing_mousePos[0], 0, 'has correct uniforms'); + } + } + ]; + + testLayer({Layer: ScatterplotLayer, testCases, onError: t.notOk}); + + t.end(); +}); diff --git a/test/modules/extensions/index.js b/test/modules/extensions/index.js index db3877511c8..8328db8404a 100644 --- a/test/modules/extensions/index.js +++ b/test/modules/extensions/index.js @@ -1 +1,2 @@ +import './brushing.spec'; import './data-filter.spec'; From 3e59402b640803b111b39f3496294ccb61f16e5f Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 2 Jul 2019 16:33:13 -0700 Subject: [PATCH 3/4] tests and docs --- docs/extensions/brushing-extension.md | 102 ++++++++++++++++++ examples/website/brushing/app.js | 63 ++++------- modules/core/src/lib/deck.js | 4 + modules/core/src/lib/layer-manager.js | 1 + modules/core/src/passes/layers-pass.js | 1 + modules/extensions/src/brushing/brushing.js | 24 ++++- .../extensions/src/brushing/shader-module.js | 33 +++--- test/modules/extensions/brushing.spec.js | 5 +- 8 files changed, 168 insertions(+), 65 deletions(-) create mode 100644 docs/extensions/brushing-extension.md diff --git a/docs/extensions/brushing-extension.md b/docs/extensions/brushing-extension.md new file mode 100644 index 00000000000..64bc3b6d373 --- /dev/null +++ b/docs/extensions/brushing-extension.md @@ -0,0 +1,102 @@ + +# BrushingExtension (experimental) + +The `BrushingExtension` adds GPU-based data brushing functionalities to layers. It allows the layer to show/hide objects based on the current pointer position. + +```js +import {ScatterplotLayer} from '@deck.gl/layers'; +import {BrushingExtension} from '@deck.gl/extensions'; + +const layer = new ScatterplotLayer({ + id: 'points', + data: POINTS, + + // props from ScatterplotLayer + getPosition: d => d.position, + getRadius: d => d.radius, + + // props added by BrushingExtension + brushingEnabled: true, + brushingRadius: 100000, + + // Define extensions + extensions: [new BrushingExtension()] +}); +``` + +## Installation + +To install the dependencies from NPM: + +```bash +npm install deck.gl +# or +npm install @deck.gl/core @deck.gl/layers @deck.gl/extensions +``` + +```js +import {BrushingExtension} from '@deck.gl/extensions'; +new BrushingExtension(); +``` + +To use pre-bundled scripts: + +```html + + + + + +``` + +```js +new deck.BrushingExtension(); +``` + +## Constructor + +```js +new BrushingExtension(); +``` + + +## Layer Properties + +When added to a layer via the `extensions` prop, the `BrushingExtension` adds the following properties to the layer: + + +##### `brushingRadius` (Number) + +The brushing radius centered at the pointer, in meters. If a data object is within this circle, it is rendered; otherwise it is hidden. + + +##### `brushingEnabled` (Boolean, optional) + +* Default: `true` + +Enable/disable brushing. If brushing is disabled, all objects are rendered. + +Brushing is always disabled when the pointer leaves the current viewport. + + +##### `brushingTarget` (Enum, optional) + +* Default: `source` + +The position used to filter each object by. One of the following: + +- `'source'`: Use the primary position for each object. This can mean different things depending on the layer. It usually refers to the coordinates returned by `getPosition` or `getSourcePosition` accessors. +- `'target'`: Use the secondary position for each object. This may not be available in some layers. It usually refers to the coordinates returned by `getTargetPosition` accessors. +- `'custom'`: Some layers may not describe their data objects with one or two coordinates, for example `PathLayer` and `PolygonLayer`. Use this option with the `getBrushingTarget` prop to provide a custom position that each object should be filtered by. + + +##### `getBrushingTarget` ([Function](/docs/developer-guide/using-layers.md#accessors), optional) + +* Default: `null` + +Called to retrieve an arbitrary position for each object that it will be filtered by. Returns an array `[x, y]`. Only effective if `brushingTarget` is set to `custom`. + + +## Source + +[modules/extensions/src/brushing](https://github.com/uber/deck.gl/tree/master/modules/extensions/src/brushing) diff --git a/examples/website/brushing/app.js b/examples/website/brushing/app.js index 2f2d85c42d2..a8569536b7e 100644 --- a/examples/website/brushing/app.js +++ b/examples/website/brushing/app.js @@ -52,11 +52,8 @@ export class App extends Component { arcs: [], targets: [], sources: [], - mousePosition: null, ...this._getLayerData(props) }; - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseLeave = this._onMouseLeave.bind(this); this._onHover = this._onHover.bind(this); } @@ -68,16 +65,6 @@ export class App extends Component { } } - _onMouseMove(evt) { - if (evt.nativeEvent) { - this.setState({mousePosition: [evt.nativeEvent.offsetX, evt.nativeEvent.offsetY]}); - } - } - - _onMouseLeave() { - this.setState({mousePosition: null}); - } - _onHover({x, y, object}) { this.setState({x, y, hoveredObject: object}); } @@ -174,10 +161,7 @@ export class App extends Component { opacity = 0.7 } = this.props; - const {arcs, targets, sources, mousePosition} = this.state; - - const isMouseover = mousePosition !== null; - const startBrushing = Boolean(isMouseover && enableBrushing); + const {arcs, targets, sources} = this.state; if (!arcs || !targets) { return null; @@ -188,12 +172,11 @@ export class App extends Component { id: 'sources', data: sources, brushingRadius: brushRadius, - mousePosition, opacity: 1, - brushingEnabled: startBrushing, + brushingEnabled: enableBrushing, pickable: false, // only show source points when brushing - radiusScale: startBrushing ? 3000 : 0, + radiusScale: enableBrushing ? 3000 : 0, getFillColor: d => (d.gain > 0 ? TARGET_COLOR : SOURCE_COLOR), extensions: [brushingExtension] }), @@ -201,14 +184,13 @@ export class App extends Component { id: 'targets-ring', data: targets, brushingRadius: brushRadius, - mousePosition, lineWidthMinPixels: 2, stroked: true, filled: false, opacity: 1, - brushingEnabled: startBrushing, + brushingEnabled: enableBrushing, // only show rings when brushing - radiusScale: startBrushing ? 4000 : 0, + radiusScale: enableBrushing ? 4000 : 0, getLineColor: d => (d.net > 0 ? TARGET_COLOR : SOURCE_COLOR), extensions: [brushingExtension] }), @@ -216,9 +198,8 @@ export class App extends Component { id: 'targets', data: targets, brushingRadius: brushRadius, - mousePosition, opacity: 1, - brushingEnabled: startBrushing, + brushingEnabled: enableBrushing, pickable: true, radiusScale: 3000, onHover: this._onHover, @@ -231,8 +212,7 @@ export class App extends Component { getWidth: strokeWidth, opacity, brushingRadius: brushRadius, - brushingEnabled: startBrushing, - mousePosition, + brushingEnabled: enableBrushing, getSourcePosition: d => d.source, getTargetPosition: d => d.target, getSourceColor: SOURCE_COLOR, @@ -246,22 +226,21 @@ export class App extends Component { const {mapStyle = 'mapbox://styles/mapbox/light-v9'} = this.props; return ( -
- {this._renderTooltip()} + + - - - -
+ {this._renderTooltip()} + ); } } diff --git a/modules/core/src/lib/deck.js b/modules/core/src/lib/deck.js index cadbc8c9b36..9a38a1fc6db 100644 --- a/modules/core/src/lib/deck.js +++ b/modules/core/src/lib/deck.js @@ -525,6 +525,10 @@ export default class Deck { _pickRequest.radius = this.props.pickingRadius; } + if (this.layerManager) { + this.layerManager.context.mousePosition = {x: _pickRequest.x, y: _pickRequest.y}; + } + _pickRequest.callback = callback; _pickRequest.event = event; _pickRequest.mode = mode; diff --git a/modules/core/src/lib/layer-manager.js b/modules/core/src/lib/layer-manager.js index 62cb2d8ba57..8ca0582c7db 100644 --- a/modules/core/src/lib/layer-manager.js +++ b/modules/core/src/lib/layer-manager.js @@ -58,6 +58,7 @@ const INITIAL_CONTEXT = Object.seal({ pickingFBO: null, // Screen-size framebuffer that layers can reuse animationProps: null, + mousePosition: null, userData: {} // Place for any custom app `context` }); diff --git a/modules/core/src/passes/layers-pass.js b/modules/core/src/passes/layers-pass.js index d44e507eec3..79bfdc48133 100644 --- a/modules/core/src/passes/layers-pass.js +++ b/modules/core/src/passes/layers-pass.js @@ -158,6 +158,7 @@ export default class LayersPass extends Pass { getModuleParameters(layer) { const moduleParameters = Object.assign(Object.create(layer.props), { viewport: layer.context.viewport, + mousePosition: layer.context.mousePosition, pickingActive: 0, devicePixelRatio: this.props.pixelRatio }); diff --git a/modules/extensions/src/brushing/brushing.js b/modules/extensions/src/brushing/brushing.js index b742fc5dd0c..985bbf27bd2 100644 --- a/modules/extensions/src/brushing/brushing.js +++ b/modules/extensions/src/brushing/brushing.js @@ -24,8 +24,6 @@ import shaderModule from './shader-module'; const defaultProps = { getBrushingTarget: {type: 'accessor', value: [0, 0]}, - mousePosition: null, - brushingTarget: 'source', brushingEnabled: true, brushingRadius: 10000 @@ -59,6 +57,28 @@ export default class BrushingExtension extends LayerExtension { } }); } + + // Trigger redraw when mouse moves + // TODO - expose this in a better way + extension.onMouseMove = () => { + this.getCurrentLayer().setNeedsRedraw(); + }; + if (this.context.deck) { + this.context.deck.eventManager.on({ + pointermove: extension.onMouseMove, + pointerleave: extension.onMouseMove + }); + } + } + + finalizeState(extension) { + // Remove event listeners + if (this.context.deck) { + this.context.deck.eventManager.off({ + pointermove: extension.onMouseMove, + pointerleave: extension.onMouseMove + }); + } } useConstantTargetPositions(attribute) { diff --git a/modules/extensions/src/brushing/shader-module.js b/modules/extensions/src/brushing/shader-module.js index 666e87c9ca9..bb650955de2 100644 --- a/modules/extensions/src/brushing/shader-module.js +++ b/modules/extensions/src/brushing/shader-module.js @@ -20,8 +20,6 @@ import {createModuleInjection} from '@luma.gl/core'; const vs = ` - const float R_EARTH = 6371000.; // earth radius in km - uniform bool brushing_enabled; uniform int brushing_target; uniform vec2 brushing_mousePos; @@ -35,25 +33,15 @@ const vs = ` varying float brushing_isVisible; - // approximate distance between lng lat in meters - float distanceBetweenLatLng(vec2 source, vec2 target) { - vec2 delta = (source - target) * PI / 180.; - - float a = - sin(delta.y / 2.) * sin(delta.y / 2.) + - cos(source.y * PI / 180.) * cos(target.y * PI / 180.) * - sin(delta.x / 2.) * sin(delta.x / 2.); - - float c = 2. * atan(sqrt(a), sqrt(1. - a)); - - return R_EARTH * c; - } - bool brushing_isPointInRange(vec2 position) { if (!brushing_enabled) { return true; } - return distanceBetweenLatLng(position, brushing_mousePos) <= brushing_radius; + vec2 source_commonspace = project_position(position); + vec2 target_commonspace = project_position(brushing_mousePos); + float distance = length((target_commonspace - source_commonspace) / project_uCommonUnitsPerMeter.xy); + + return distance <= brushing_radius; } void brushing_setVisible(bool visible) { @@ -116,13 +104,18 @@ export default { brushingEnabled = true, brushingRadius = 10000, brushingTarget = 'source', - mousePosition = null + mousePosition, + viewport } = opts; return { - brushing_enabled: Boolean(brushingEnabled && mousePosition), + brushing_enabled: Boolean( + brushingEnabled && mousePosition && viewport.containsPixel(mousePosition) + ), brushing_radius: brushingRadius, brushing_target: TARGET[brushingTarget] || 0, - brushing_mousePos: mousePosition ? opts.viewport.unproject(mousePosition) : [0, 0] + brushing_mousePos: mousePosition + ? viewport.unproject([mousePosition.x - viewport.x, mousePosition.y - viewport.y]) + : [0, 0] }; } }; diff --git a/test/modules/extensions/brushing.spec.js b/test/modules/extensions/brushing.spec.js index 0ffdf0fc3fb..6d3e462ee69 100644 --- a/test/modules/extensions/brushing.spec.js +++ b/test/modules/extensions/brushing.spec.js @@ -27,10 +27,13 @@ test('BrushingExtension', t => { { updateProps: { brushingEnabled: true, - mousePosition: [1, 1], brushingTarget: 'custom', brushingRadius: 5e6 }, + onBeforeUpdate: ({layer}) => { + // Simulate user interaction + layer.context.mousePosition = {x: 1, y: 1}; + }, onAfterUpdate: ({layer}) => { const {uniforms} = layer.state.model.program; t.is(uniforms.brushing_radius, 5e6, 'has correct uniforms'); From 37b5841ccefc3541b57ac33ae4b21ec0c4700fda Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Wed, 3 Jul 2019 14:02:41 -0700 Subject: [PATCH 4/4] address comments --- modules/core/src/lib/deck.js | 32 ++++------------- modules/google-maps/src/utils.js | 4 ++- test/modules/extensions/fp64.spec.js | 34 +++++++++++++++++++ .../google-maps/google-maps-overlay.spec.js | 8 ++--- 4 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 test/modules/extensions/fp64.spec.js diff --git a/modules/core/src/lib/deck.js b/modules/core/src/lib/deck.js index 9a38a1fc6db..1c6e964c517 100644 --- a/modules/core/src/lib/deck.js +++ b/modules/core/src/lib/deck.js @@ -149,7 +149,6 @@ export default class Deck { this._onEvent = this._onEvent.bind(this); this._onPointerDown = this._onPointerDown.bind(this); this._onPointerMove = this._onPointerMove.bind(this); - this._onPointerLeave = this._onPointerLeave.bind(this); this._pickAndCallback = this._pickAndCallback.bind(this); this._onRendererInitialized = this._onRendererInitialized.bind(this); this._onRenderFrame = this._onRenderFrame.bind(this); @@ -507,12 +506,15 @@ export default class Deck { // The `pointermove` event may fire multiple times in between two animation frames, // it's a waste of time to run picking without rerender. Instead we save the last pick // request and only do it once on the next animation frame. - _requestPick({event, callback, mode}) { + _onPointerMove(event) { const {_pickRequest} = this; if (event.type === 'pointerleave') { _pickRequest.x = -1; _pickRequest.y = -1; _pickRequest.radius = 0; + } else if (event.leftButton || event.rightButton) { + // Do not trigger onHover callbacks if mouse button is down. + return; } else { const pos = event.offsetCenter; // Do not trigger callbacks when click/hover position is invalid. Doing so will cause a @@ -529,9 +531,9 @@ export default class Deck { this.layerManager.context.mousePosition = {x: _pickRequest.x, y: _pickRequest.y}; } - _pickRequest.callback = callback; + _pickRequest.callback = this.props.onHover; _pickRequest.event = event; - _pickRequest.mode = mode; + _pickRequest.mode = 'hover'; } // Actually run picking @@ -596,7 +598,7 @@ export default class Deck { events: { pointerdown: this._onPointerDown, pointermove: this._onPointerMove, - pointerleave: this._onPointerLeave + pointerleave: this._onPointerMove } }); for (const eventType in EVENTS) { @@ -771,26 +773,6 @@ export default class Deck { }); } - _onPointerMove(event) { - if (event.leftButton || event.rightButton) { - // Do not trigger onHover callbacks if mouse button is down. - return; - } - this._requestPick({ - callback: this.props.onHover, - event, - mode: 'hover' - }); - } - - _onPointerLeave(event) { - this._requestPick({ - callback: this.props.onHover, - event, - mode: 'hover' - }); - } - _getFrameStats() { this.stats.get('frameRate').timeEnd(); this.stats.get('frameRate').timeStart(); diff --git a/modules/google-maps/src/utils.js b/modules/google-maps/src/utils.js index 4d43016fc40..0f8acb398ea 100644 --- a/modules/google-maps/src/utils.js +++ b/modules/google-maps/src/utils.js @@ -142,11 +142,13 @@ function handleMouseEvent(deck, type, event) { break; case 'mousemove': + type = 'pointermove'; callback = deck._onPointerMove; break; case 'mouseout': - callback = deck._onPointerLeave; + type = 'pointerleave'; + callback = deck._onPointerMove; break; default: diff --git a/test/modules/extensions/fp64.spec.js b/test/modules/extensions/fp64.spec.js new file mode 100644 index 00000000000..7200b5b79d2 --- /dev/null +++ b/test/modules/extensions/fp64.spec.js @@ -0,0 +1,34 @@ +import test from 'tape-catch'; +import {Fp64Extension} from '@deck.gl/extensions'; +import {COORDINATE_SYSTEM} from '@deck.gl/core'; +import {ScatterplotLayer} from '@deck.gl/layers'; +import {testLayer} from '@deck.gl/test-utils'; + +test('Fp64Extension', t => { + const testCases = [ + { + props: { + id: 'fp64-test', + data: [ + {position: [-122.453, 37.782], timestamp: 120, entry: 13567, exit: 4802}, + {position: [-122.454, 37.781], timestamp: 140, entry: 14475, exit: 5493} + ], + getPosition: d => d.position + } + }, + { + updateProps: { + coordinateSystem: COORDINATE_SYSTEM.LNGLAT_DEPRECATED, + extensions: [new Fp64Extension()] + }, + onAfterUpdate: ({layer}) => { + const {uniforms} = layer.state.model.program; + t.ok(uniforms.project_uViewProjectionMatrixFP64, 'has fp64 uniforms'); + } + } + ]; + + testLayer({Layer: ScatterplotLayer, testCases, onError: t.notOk}); + + t.end(); +}); diff --git a/test/modules/google-maps/google-maps-overlay.spec.js b/test/modules/google-maps/google-maps-overlay.spec.js index 3e73cd08ae0..7a840ab99dd 100644 --- a/test/modules/google-maps/google-maps-overlay.spec.js +++ b/test/modules/google-maps/google-maps-overlay.spec.js @@ -80,13 +80,11 @@ test('GoogleMapsOverlay#draw, pick', t => { const pointerMoveSpy = makeSpy(overlay._deck, '_onPointerMove'); map.emit({type: 'mousemove', pixel: [0, 0]}); - t.ok(pointerMoveSpy.called, 'pointer move event is handled'); - pointerMoveSpy.reset(); + t.is(pointerMoveSpy.callCount, 1, 'pointer move event is handled'); - const pointerLeaveSpy = makeSpy(overlay._deck, '_onPointerLeave'); map.emit({type: 'mouseout', pixel: [0, 0]}); - t.ok(pointerLeaveSpy.called, 'pointer leave event is handled'); - pointerLeaveSpy.reset(); + t.is(pointerMoveSpy.callCount, 2, 'pointer leave event is handled'); + pointerMoveSpy.reset(); overlay.finalize();