Skip to content

Commit

Permalink
AggregationLayer: add support for aggregation change detection.
Browse files Browse the repository at this point in the history
  • Loading branch information
1chandu committed Dec 18, 2019
1 parent 7889163 commit 911eff1
Show file tree
Hide file tree
Showing 23 changed files with 653 additions and 407 deletions.
111 changes: 80 additions & 31 deletions modules/aggregation-layers/src/aggregation-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,16 @@

import {CompositeLayer, AttributeManager, _compareProps as compareProps} from '@deck.gl/core';
import {cssToDeviceRatio} from '@luma.gl/core';

// props when changed results in new uniforms that requires re-aggregation
const UNIFORM_PROPS = [
// DATA-FILTER extension
'filterEnabled',
'filterRange',
'filterSoftRange',
'filterTransformSize',
'filterTransformColor'
];
import {filterProps} from './utils/prop-utils';

export default class AggregationLayer extends CompositeLayer {
initializeState(aggregationProps = []) {
initializeState(dimensions) {
super.initializeState();

this.setState({
aggregationProps: aggregationProps.concat(UNIFORM_PROPS)
// Layer props , when changed doesn't require updating aggregation
ignoreProps: filterProps(this.constructor._propTypes, dimensions.data.props),
dimensions
});
}

Expand All @@ -55,13 +49,9 @@ export default class AggregationLayer extends CompositeLayer {
}

updateAttributes(changedAttributes) {
let dataChanged = false;
// eslint-disable-next-line
for (const name in changedAttributes) {
dataChanged = true;
break;
}
this.setState({dataChanged});
// Super classes, can refer to state.changedAttributes to determine what
// attributes changed
this.setState({changedAttributes});
}

getAttributes() {
Expand All @@ -86,20 +76,65 @@ export default class AggregationLayer extends CompositeLayer {
// Default implemention is empty, subclasses can update their Model objects if needed
}

isAggregationDirty(opts) {
if (this.state.dataChanged || opts.changeFlags.extensionsChanged) {
return true;
/**
* Checks if aggregation is dirty
* @param {Object} updateOpts - object {props, oldProps, changeFlags}
* @param {Object} params - object {dimension, compareAll}
* @param {Object} params.dimension - {props, accessors} array of props and/pr accessors
* @param {Boolean} params.compareAll - when `true` it will include non layer props for comparision
* @returns {Boolean} - returns true if dimensions' prop or accessor is changed
**/
isAggregationDirty(updateOpts, params = {}) {
const {props, oldProps, changeFlags} = updateOpts;
const {compareAll = false, dimension} = params;
const {ignoreProps} = this.state;
const {props: dataProps, accessors = []} = dimension;
const {updateTriggersChanged} = changeFlags;
if (updateTriggersChanged) {
if (updateTriggersChanged.all) {
return true;
}
for (const accessor of accessors) {
if (updateTriggersChanged[accessor]) {
return true;
}
}
}
if (compareAll) {
if (changeFlags.extensionsChanged) {
return true;
}
// Compare non layer props too (like extension props)
// ignoreprops refers to all Layer props other than aggregation props that need to be comapred
return compareProps({
oldProps,
newProps: props,
ignoreProps,
propTypes: this.constructor._propTypes
});
}
const {aggregationProps} = this.state;
const oldProps = {};
const props = {};
for (const propName of aggregationProps) {
oldProps[propName] = opts.oldProps[propName];
props[propName] = opts.props[propName];
// Compare props of the dimension
for (const name of dataProps) {
if (props[name] !== oldProps[name]) {
return true;
}
}
return Boolean(
compareProps({oldProps, newProps: props, propTypes: this.constructor._propTypes})
);
return false;
}

/**
* Checks if an attribute is changed
* @param {String} name - name of the attribute
* @returns {Boolean} - `true` if attribute `name` is changed, `false` otherwise,
* If `name` is not passed or `undefiend`, `true` if any attribute is changed, `false` otherwise
**/
isAttributeChanged(name) {
const {changedAttributes} = this.state;
if (!name) {
// if name not specified return true if any attribute is changed
return !isObjectEmpty(changedAttributes);
}
return changedAttributes && changedAttributes[name] !== undefined;
}

// Private
Expand All @@ -113,4 +148,18 @@ export default class AggregationLayer extends CompositeLayer {
}
}

// Helper methods

// Returns true if given object is empty, false otherwise.
function isObjectEmpty(obj) {
let isEmpty = true;
/* eslint-disable no-unused-vars */
for (const key in obj) {
isEmpty = false;
break;
}
/* eslint-enable no-unused-vars */
return isEmpty;
}

AggregationLayer.layerName = 'AggregationLayer';
37 changes: 33 additions & 4 deletions modules/aggregation-layers/src/aggregation-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ All of the layers in `@deck.gl/aggregation-layers` module perform some sort of d

`AggregationLayer` is subclassed form `CompositeLayer` and all layers in `@deck.gl/aggregation-layers` are subclassed from this Layer.

### Integration with `AttributeManager`
## Integration with `AttributeManager`

This layer creates `AttributeManager` and makes it available for its subclasses. Any aggregation layer can add attributes to the `AttributeManager` and retrieve them using `getAttributes` method. This enables using `AttributeManager`'s features and optimization for using attributes. Also manual iteration of `data` prop can be removed and attributes can be directly set on GPU aggregation models or accessed directly for CPU aggregation.

Expand All @@ -22,14 +22,43 @@ attributeManager.add({
});
```

### updateState()
## updateState()

During update state, Subclasses of `AggregationLayer` must first call 'super.updateState()', which calls

- `updateShaders(shaders)` : Subclasses can override this if they need to update shaders, for example, when performing GPU aggregation, aggregation shaders must be merged with argument of this function to correctly apply `extensions`.

- `_updateAttributes`: This checks and updates attributes based on updated props.

### Checking if aggregation is dirty
## Checking if aggregation is dirty

Constructor, takes an array of props, `aggregationProps`, and a private method `isAggregationDirty()` is provided that returns `true` when any of the props in `aggregationProps` are changed. Subclasses can customize this to desired props by providing `aggregatinProps` array.
### Dimensions

Typical aggregation, involves :
1. Group the input data points into bins
2. Compute the aggregated value for each bin

For example, when `cellSize` or `data` is changed, layer needs to perform both `1` and `2` steps, when a parameter affecting a bin's value is changed (like `getWeight` accessor), layer only need to perform step `2`.

When doing CPU Aggregation, both above steps are performed individually. But for GPU aggregation, both are merged into single render call.

To support what state is dirty, constructor takes `dimensions` object, which contains, several keyed dimensions. It must contain `data` dimension that defines, when re-aggregation needs to be performed.

### isAggregationDirty()

This helper can be used if a dimension is changed. Sublayers can defined custom dimensions and call this method to check if a dimension is changed.


### isAttributeChanged()

`AggregationLayer` tracks what attributes are changed in each update cycle. Super classes can use `isAttributeChanged()` method to check if a specific attribute is changed or any attribute is changed.

#### Aggregation State

`AggregationLayer` is responsible for setting following values in `sate` object:

* `positionsChanged` : Set to `true` when the position attribute is changed. Super layers must set `state.positionAttributeName`, to define the name of position attribute, by default `positions` is used as name of position attribute. This flag, can be used to re compute aggregation parameters that depend on position data, such as bounding-box etc.

* `attributesChanged` : Set to `true`, when any of the attributes are changed. This flag can be used to re-trigger aggregation.

It is up to the subclasses how to use these flags, based on aggregation needs. For example, GPU aggregation layer can trigger re aggregation when `attributesChanged`.
112 changes: 98 additions & 14 deletions modules/aggregation-layers/src/contour-layer/contour-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {generateContours} from './contour-utils';
import {log} from '@deck.gl/core';

import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator';
import {AGGREGATION_OPERATION} from '../utils/aggregation-operation-utils';
import {AGGREGATION_OPERATION, getValueFunc} from '../utils/aggregation-operation-utils';
import {getBoundingBox, getGridParams} from '../utils/grid-aggregation-utils';
import GridAggregationLayer from '../grid-aggregation-layer';

const DEFAULT_COLOR = [255, 255, 255, 255];
Expand All @@ -45,14 +46,26 @@ const defaultProps = {
zOffset: 0.005
};

// props , when changed requires re-aggregation
const AGGREGATION_PROPS = ['aggregation', 'getWeight'];
const POSITION_ATTRIBUTE_NAME = 'positions';

const DIMENSIONS = {
data: {
props: ['cellSize']
},
weights: {
props: ['aggregation'],
accessors: ['getWeight']
}
};

export default class ContourLayer extends GridAggregationLayer {
initializeState() {
super.initializeState({aggregationProps: AGGREGATION_PROPS});
super.initializeState({
dimensions: DIMENSIONS
});
this.setState({
contourData: {},
projectPoints: false,
weights: {
count: {
size: 1,
Expand All @@ -62,7 +75,7 @@ export default class ContourLayer extends GridAggregationLayer {
});
const attributeManager = this.getAttributeManager();
attributeManager.add({
positions: {
[POSITION_ATTRIBUTE_NAME]: {
size: 3,
accessor: 'getPosition',
type: GL.DOUBLE,
Expand Down Expand Up @@ -132,8 +145,12 @@ export default class ContourLayer extends GridAggregationLayer {

// Aggregation Overrides

updateAggregationFlags({props, oldProps}) {
const cellSizeChanged = oldProps.cellSize !== props.cellSize;
/* eslint-disable max-statements, complexity */
updateAggregationState(opts) {
const {props, oldProps} = opts;
const {cellSize, coordinateSystem} = props;
const {viewport} = this.context;
const cellSizeChanged = oldProps.cellSize !== cellSize;
let gpuAggregation = props.gpuAggregation;
if (this.state.gpuAggregation !== props.gpuAggregation) {
if (gpuAggregation && !GPUGridAggregator.isSupported(this.context.gl)) {
Expand All @@ -142,16 +159,83 @@ export default class ContourLayer extends GridAggregationLayer {
}
}
const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation;
// Consider switching between CPU and GPU aggregation as data changed as it requires
// re aggregation.
const dataChanged = this.state.dataChanged || gpuAggregationChanged;
this.setState({
dataChanged,
cellSizeChanged,
cellSize: props.cellSize,
needsReProjection: dataChanged || cellSizeChanged,
gpuAggregation
});

const {dimensions} = this.state;
const positionsChanged = this.isAttributeChanged(POSITION_ATTRIBUTE_NAME);
const {data, weights} = dimensions;

let {boundingBox} = this.state;
if (positionsChanged) {
boundingBox = getBoundingBox(this.getAttributes(), this.getNumInstances());
}
if (positionsChanged || cellSizeChanged) {
const {
gridOffset,
boundingBoxAligned,
translation,
width,
height,
numCol,
numRow
} = getGridParams(boundingBox, cellSize, viewport, coordinateSystem);
this.allocateResources(numRow, numCol);
this.setState({
gridOffset,
boundingBox: boundingBoxAligned,
translation,
posOffset: translation.slice(), // Used for CPU aggregation, to offset points
width,
height,
numCol,
numRow
});
}

const aggregationDataDirty =
positionsChanged ||
gpuAggregationChanged ||
this.isAggregationDirty(opts, {
dimension: data,
compareAll: gpuAggregation // check for all (including extentions props) when using gpu aggregation
});
const aggregationWeightsDirty = this.isAggregationDirty(opts, {
dimension: weights
});

if (aggregationWeightsDirty) {
this._updateAccessors(opts);
}
if (aggregationDataDirty || aggregationWeightsDirty) {
this._resetResults();
}
this.setState({
aggregationDataDirty,
aggregationWeightsDirty
});
}
/* eslint-enable max-statements, complexity */

// Private (Aggregation)

_updateAccessors(opts) {
const {getWeight, aggregation} = opts.props;
const {count} = this.state.weights;
if (this.state.gpuAggregation) {
count.getWeight = getWeight;
count.operation = AGGREGATION_OPERATION[aggregation];
} else {
this.setState({getValue: getValueFunc(aggregation, getWeight)});
}
}

_resetResults() {
const {count} = this.state.weights;
if (count) {
count.aggregationData = null;
}
}

// Private (Contours)
Expand Down
Loading

0 comments on commit 911eff1

Please sign in to comment.