Skip to content

Commit

Permalink
Formal API for CompositeLayer data/accessor wrapping (visgl#3311)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Jul 8, 2019
1 parent 2774c95 commit 42fd04e
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 213 deletions.
27 changes: 27 additions & 0 deletions docs/api-reference/composite-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,33 @@ Returns:

Constructor for this sublayer. The base class implementation checks if `type` is specified for the sublayer in `_subLayerProps`, otherwise returns the default.

##### `getSubLayerRow`

Used by [adapter layers](/docs/developer-guide/custom-layers/composite-layers.md#transforming-data)) to decorate transformed data with a reference to the original object.

Parameters:

* `row` (Object) - a custom data object to pass to a sublayer.
* `sourceObject` (Object) - the original data object provided by the user
* `sourceObjectIndex` (Object) - the index of the original data object provided by the user

Returns:

The `row` object, decorated with a reference.


##### `getSubLayerAccessor`

Used by [adapter layers](/docs/developer-guide/custom-layers/composite-layers.md#transforming-data)) to allow user-provided accessors read the original objects from transformed data.

Parameters:

* `accessor` (Function|Any) - the accessor provided to the current layer.

Returns:

* If `accessor` is a function, returns a new accessor function.
* If `accessor` is a constant value, returns it as is.

## Source

Expand Down
67 changes: 67 additions & 0 deletions docs/developer-guide/custom-layers/composite-layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,70 @@ class AwesomeCompositeLayer extends CompositeLayer {
```

For more details, read about [how picking works](/docs/developer-guide/custom-layers/picking.md).


### Transforming Data

Because deck.gl's primitive layers expect input to be a flat iteratorable data structure, some composite layers need to transform user data into a different format before passing to sublayers. This transformation may consist converting a tree to an array, filtering, sorting, etc. For example, the [GeoJsonLayer](/docs/layers/geojson-layer.md) splits features by type and passes each to `ScatterplotLayer`, `PathLayer` or `SolidPolygonLayer` respectively. The [TextLayer](/docs/layers/text-layer.md) breaks each text string down to multiple characters and render them with a variation of `IconLayer`.

From the user's perspective, when they specify accessors such as `getColor`, or callbacks such as `onHover`, the functions should always interface with the original data that they give the top-level layer, instead of its internal implementations. For the sublayer to reference back to the original data, we can add a reference onto every transformed datum by calling `getSubLayerRow`:

```js
class MyCompositeLayer extends CompositeLayer {
updateState({props, changeFlags}) {
if (changeFlags.dataChanged) {
// data to pass to the sublayer
const subLayerData = [];
/*
* input data format:
[
{position: [-122.45, 37.78], timestamps: [0, 1, 4, 7, 8]},
{position: [-122.43, 38.01], timestamps: [2, 4]},
...
]
* data format to pass to sublayer:
[
{timestamp: 0},
{timestamp: 1},
{timestamp: 4},
{timestamp: 7},
...
]
*/
props.data.forEach((object, index) => {
for (const timestamp of object.timestamps) {
// `getSubLayerRow` decorates each data row for the sub layer with a reference to the original object and index
subLayerData.push(this.getSubLayerRow({
timestamp
}, object, index));
}
});

this.setState({subLayerData});
}
}
}
```

When the sublayer receives data decorated by `getSubLayerRow`, its accessors need to know how to read the data to access the original objects. In the above example, `getPosition: d => d.position` would fail if called with `{timestamp: 0}`, while the user expects it to be called with `{position: [-122.45, 37.78], timestamps: [0, 1, 4, 7, 8]}`. This can be solved by wrapping the user-provided accessor with `getSubLayerAccessor`:

```js
renderLayers() {
const {subLayerData} = this.state;
const {getPosition, getRadius, getFillColor, getLineColor, getLineWidth, updateTriggers} = this.props;

return new ScatterplotLayer(props, this.getSubLayerProps({
id: 'scatterplot',
updateTriggers,

data: this.state.subLayerData,
getPosition: this.getSubLayerAccessor(getPosition),
getRadius: this.getSubLayerAccessor(getRadius),
getFillColor: this.getSubLayerAccessor(getFillColor),
getLineColor: this.getSubLayerAccessor(getLineColor),
getLineWidth: this.getSubLayerAccessor(getLineWidth)
}));
}
```

The default implementations of lifecycle methods such as `getPickingInfo` also understand how to retrieve the orignal objects from the sublayer data if they are created using `getSubLayerRow`.
45 changes: 44 additions & 1 deletion modules/core/src/lib/composite-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,19 @@ export default class CompositeLayer extends Layer {
// not apply to a composite layer.
// @return null to cancel event
getPickingInfo({info}) {
return info;
const {object} = info;
const isDataWrapped =
object && object.__source && object.__source.parent && object.__source.parent.id === this.id;

if (!isDataWrapped) {
return info;
}

return Object.assign(info, {
// override object with picked data
object: object.__source.object,
index: object.__source.index
});
}

// Implement to generate subLayers
Expand All @@ -69,6 +81,37 @@ export default class CompositeLayer extends Layer {
);
}

// When casting user data into another format to pass to sublayers,
// add reference to the original object and object index
getSubLayerRow(row, sourceObject, sourceObjectIndex) {
row.__source = {
parent: this,
object: sourceObject,
index: sourceObjectIndex
};
return row;
}

// Some composite layers cast user data into another format before passing to sublayers
// We need to unwrap them before calling the accessor so that they see the original data
// objects
getSubLayerAccessor(accessor) {
if (typeof accessor === 'function') {
const objectInfo = {
data: this.props.data,
target: []
};
return (x, i) => {
if (x.__source) {
objectInfo.index = x.__source.index;
return accessor(x.__source.object, objectInfo);
}
return accessor(x, i);
};
}
return accessor;
}

// Returns sub layer props for a specific sublayer
getSubLayerProps(sublayerProps) {
const {
Expand Down
9 changes: 2 additions & 7 deletions modules/core/src/lib/layer-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,8 @@ export class LayerExtension {
newProps[key] = propValue;
if (propDef && propDef.type === 'accessor') {
newProps.updateTriggers[key] = this.props.updateTriggers[key];
// Some composite layers "wrap" user data into another format before passing to sublayers
// We need to unwrap them before calling the accessor so that they see the original data
// objects
// TODO - make this a formal API and document
if (this.unwrapObject && typeof propValue === 'function') {
newProps[key] = (object, objectInfo) =>
propValue(this.unwrapObject(object), objectInfo);
if (typeof propValue === 'function') {
newProps[key] = this.getSubLayerAccessor(propValue, true);
}
}
}
Expand Down
21 changes: 1 addition & 20 deletions modules/geo-layers/src/h3-layers/h3-cluster-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,14 @@ export default class H3ClusterLayer extends CompositeLayer {
const multiPolygon = h3SetToMultiPolygon(hexagons, true);

for (const polygon of multiPolygon) {
polygons.push({polygon, _obj: object, _idx: objectInfo.index});
polygons.push(this.getSubLayerRow({polygon}, object, objectInfo.index));
}
}

this.setState({polygons});
}
}

getPickingInfo({info}) {
return Object.assign(info, {
object: info.object && info.object._obj,
index: info.object && info.object._idx
});
}

unwrapObject(object) {
return object._obj;
}

getSubLayerAccessor(accessor) {
if (typeof accessor !== 'function') return accessor;

return (object, objectInfo) => {
return accessor(object._obj, objectInfo);
};
}

renderLayers() {
const {
elevationScale,
Expand Down
64 changes: 18 additions & 46 deletions modules/layers/src/geojson-layer/geojson-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ import {PhongMaterial} from '@luma.gl/core';
import SolidPolygonLayer from '../solid-polygon-layer/solid-polygon-layer';
import {replaceInRange} from '../utils';

import {
getGeojsonFeatures,
separateGeojsonFeatures,
unwrapSourceFeature,
unwrapSourceFeatureIndex
} from './geojson';
import {getGeojsonFeatures, separateGeojsonFeatures} from './geojson';

const defaultLineColor = [0, 0, 0, 255];
const defaultFillColor = [0, 0, 0, 255];
Expand Down Expand Up @@ -78,15 +73,6 @@ function getCoordinates(f) {
return f.geometry.coordinates;
}

/**
* Unwraps the real source feature passed into props and passes as the argument to `accessor`.
*/
function unwrappingAccessor(accessor) {
if (typeof accessor !== 'function') return accessor;

return feature => accessor(unwrapSourceFeature(feature));
}

export default class GeoJsonLayer extends CompositeLayer {
initializeState() {
this.state = {
Expand All @@ -99,6 +85,7 @@ export default class GeoJsonLayer extends CompositeLayer {
return;
}
const features = getGeojsonFeatures(props.data);
const wrapFeature = this.getSubLayerRow.bind(this);

if (Array.isArray(changeFlags.dataChanged)) {
const oldFeatures = this.state.features;
Expand All @@ -110,12 +97,12 @@ export default class GeoJsonLayer extends CompositeLayer {
}

for (const dataRange of changeFlags.dataChanged) {
const partialFeatures = separateGeojsonFeatures(features, dataRange);
const partialFeatures = separateGeojsonFeatures(features, wrapFeature, dataRange);
for (const key in oldFeatures) {
featuresDiff[key].push(
replaceInRange({
data: newFeatures[key],
getIndex: unwrapSourceFeatureIndex,
getIndex: f => f.__source.index,
dataRange,
replace: partialFeatures[key]
})
Expand All @@ -125,27 +112,12 @@ export default class GeoJsonLayer extends CompositeLayer {
this.setState({features: newFeatures, featuresDiff});
} else {
this.setState({
features: separateGeojsonFeatures(features),
features: separateGeojsonFeatures(features, wrapFeature),
featuresDiff: {}
});
}
}

getPickingInfo({info, sourceLayer}) {
// `info.index` is the index within the particular sub-layer
// We want to expose the index of the feature the user provided

return Object.assign(info, {
// override object with picked feature
object: info.object ? unwrapSourceFeature(info.object) : info.object,
index: info.object ? unwrapSourceFeatureIndex(info.object) : info.index
});
}

unwrapObject(object) {
return unwrapSourceFeature(object);
}

/* eslint-disable complexity */
renderLayers() {
const {features, featuresDiff} = this.state;
Expand Down Expand Up @@ -197,9 +169,9 @@ export default class GeoJsonLayer extends CompositeLayer {
filled,
wireframe,
material,
getElevation: unwrappingAccessor(getElevation),
getFillColor: unwrappingAccessor(getFillColor),
getLineColor: unwrappingAccessor(getLineColor),
getElevation: this.getSubLayerAccessor(getElevation),
getFillColor: this.getSubLayerAccessor(getFillColor),
getLineColor: this.getSubLayerAccessor(getLineColor),

transitions: transitions && {
getPolygon: transitions.geometry,
Expand Down Expand Up @@ -239,9 +211,9 @@ export default class GeoJsonLayer extends CompositeLayer {
miterLimit: lineMiterLimit,
dashJustified: lineDashJustified,

getColor: unwrappingAccessor(getLineColor),
getWidth: unwrappingAccessor(getLineWidth),
getDashArray: unwrappingAccessor(getLineDashArray),
getColor: this.getSubLayerAccessor(getLineColor),
getWidth: this.getSubLayerAccessor(getLineWidth),
getDashArray: this.getSubLayerAccessor(getLineDashArray),

transitions: transitions && {
getPath: transitions.geometry,
Expand Down Expand Up @@ -277,9 +249,9 @@ export default class GeoJsonLayer extends CompositeLayer {
miterLimit: lineMiterLimit,
dashJustified: lineDashJustified,

getColor: unwrappingAccessor(getLineColor),
getWidth: unwrappingAccessor(getLineWidth),
getDashArray: unwrappingAccessor(getLineDashArray),
getColor: this.getSubLayerAccessor(getLineColor),
getWidth: this.getSubLayerAccessor(getLineWidth),
getDashArray: this.getSubLayerAccessor(getLineDashArray),

transitions: transitions && {
getPath: transitions.geometry,
Expand Down Expand Up @@ -317,10 +289,10 @@ export default class GeoJsonLayer extends CompositeLayer {
lineWidthMinPixels,
lineWidthMaxPixels,

getFillColor: unwrappingAccessor(getFillColor),
getLineColor: unwrappingAccessor(getLineColor),
getRadius: unwrappingAccessor(getRadius),
getLineWidth: unwrappingAccessor(getLineWidth),
getFillColor: this.getSubLayerAccessor(getFillColor),
getLineColor: this.getSubLayerAccessor(getLineColor),
getRadius: this.getSubLayerAccessor(getRadius),
getLineWidth: this.getSubLayerAccessor(getLineWidth),

transitions: transitions && {
getPosition: transitions.geometry,
Expand Down
Loading

0 comments on commit 42fd04e

Please sign in to comment.