@deck.gl/aggregation-layers
Advanced tools
Comparing version 9.0.35 to 9.1.0-beta.1
@@ -1,7 +0,11 @@ | ||
import { Accessor, AccessorFunction, Color, Position, UpdateParameters, DefaultProps, LayersList } from '@deck.gl/core'; | ||
import GridAggregationLayer, { GridAggregationLayerProps } from "../grid-aggregation-layer.js"; | ||
/** All properties supported by ContourLayer. */ | ||
export type ContourLayerProps<DataT = unknown> = _ContourLayerProps<DataT> & GridAggregationLayerProps<DataT>; | ||
/** Properties added by ContourLayer. */ | ||
export type _ContourLayerProps<DataT> = { | ||
import { Accessor, GetPickingInfoParams, LayersList, PickingInfo, Position, Viewport, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator, AggregationOperation } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import { AggregationLayerProps } from "../common/aggregation-layer.js"; | ||
import { Contour, ContourLine, ContourPolygon } from "./contour-utils.js"; | ||
import { BinOptions } from "./bin-options-uniforms.js"; | ||
/** All properties supported by GridLayer. */ | ||
export type ContourLayerProps<DataT = unknown> = _ContourLayerProps<DataT> & AggregationLayerProps<DataT>; | ||
/** Properties added by GridLayer. */ | ||
type _ContourLayerProps<DataT> = { | ||
/** | ||
@@ -13,4 +17,9 @@ * Size of each cell in meters. | ||
/** | ||
* The grid origin | ||
* @default [0, 0] | ||
*/ | ||
gridOrigin?: [number, number]; | ||
/** | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
* @default true | ||
* @default false | ||
*/ | ||
@@ -22,3 +31,3 @@ gpuAggregation?: boolean; | ||
*/ | ||
aggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
aggregation?: AggregationOperation; | ||
/** | ||
@@ -28,22 +37,3 @@ * Definition of contours to be drawn. | ||
*/ | ||
contours: { | ||
/** | ||
* Isolines: `threshold` value must be a single `Number`, Isolines are generated based on this threshold value. | ||
* | ||
* Isobands: `threshold` value must be an Array of two `Number`s. Isobands are generated using `[threshold[0], threshold[1])` as threshold range, i.e area that has values `>= threshold[0]` and `< threshold[1]` are rendered with corresponding color. NOTE: `threshold[0]` is inclusive and `threshold[1]` is not inclusive. | ||
*/ | ||
threshold: number | number[]; | ||
/** | ||
* RGBA color array to be used to render the contour. | ||
* @default [255, 255, 255, 255] | ||
*/ | ||
color?: Color; | ||
/** | ||
* Applicable for `Isoline`s only, width of the Isoline in pixels. | ||
* @default 1 | ||
*/ | ||
strokeWidth?: number; | ||
/** Defines z order of the contour. */ | ||
zIndex?: number; | ||
}[]; | ||
contours?: Contour[]; | ||
/** | ||
@@ -58,3 +48,3 @@ * A very small z offset that is added for each vertex of a contour (Isoline or Isoband). | ||
*/ | ||
getPosition?: AccessorFunction<DataT, Position>; | ||
getPosition?: Accessor<DataT, Position>; | ||
/** | ||
@@ -66,29 +56,31 @@ * The weight of each object. | ||
}; | ||
/** Aggregate data into iso-lines or iso-bands for a given threshold and cell size. */ | ||
export default class ContourLayer<DataT = any, ExtraPropsT extends {} = {}> extends GridAggregationLayer<DataT, ExtraPropsT & Required<_ContourLayerProps<DataT>>> { | ||
export type ContourLayerPickingInfo = PickingInfo<{ | ||
contour: Contour; | ||
}>; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends AggregationLayer<DataT, ExtraPropsT & Required<_ContourLayerProps<DataT>>> { | ||
static layerName: string; | ||
static defaultProps: DefaultProps<ContourLayerProps<unknown>>; | ||
state: GridAggregationLayer<DataT>['state'] & { | ||
contourData: { | ||
contourSegments: { | ||
start: number[]; | ||
end: number[]; | ||
contour: any; | ||
}[]; | ||
contourPolygons: { | ||
vertices: number[][]; | ||
contour: any; | ||
}[]; | ||
state: AggregationLayer<DataT>['state'] & BinOptions & { | ||
aggregatedValueReader?: (x: number, y: number) => number; | ||
contourData?: { | ||
lines: ContourLine[]; | ||
polygons: ContourPolygon[]; | ||
}; | ||
thresholdData: any; | ||
binIdRange: [number, number][]; | ||
aggregatorViewport: Viewport; | ||
}; | ||
getAggregatorType(): string; | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator; | ||
initializeState(): void; | ||
updateState(opts: UpdateParameters<this>): void; | ||
renderLayers(): LayersList; | ||
updateAggregationState(opts: UpdateParameters<this>): void; | ||
private _updateAccessors; | ||
private _resetResults; | ||
private _generateContours; | ||
private _updateThresholdData; | ||
updateState(params: UpdateParameters<this>): boolean; | ||
private _updateBinOptions; | ||
draw(opts: any): void; | ||
private _onAggregationUpdate; | ||
private _getContours; | ||
onAttributeChange(id: string): void; | ||
renderLayers(): LayersList | null; | ||
getPickingInfo(params: GetPickingInfoParams): ContourLayerPickingInfo; | ||
} | ||
export {}; | ||
//# sourceMappingURL=contour-layer.d.ts.map |
@@ -1,36 +0,22 @@ | ||
// Copyright (c) 2015 - 2018 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 { LineLayer, SolidPolygonLayer } from '@deck.gl/layers'; | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { COORDINATE_SYSTEM, project32, Viewport, _deepEqual } from '@deck.gl/core'; | ||
import { PathLayer, SolidPolygonLayer } from '@deck.gl/layers'; | ||
import { WebGLAggregator, CPUAggregator } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import { generateContours } from "./contour-utils.js"; | ||
import { log } from '@deck.gl/core'; | ||
import GPUGridAggregator from "../utils/gpu-grid-aggregation/gpu-grid-aggregator.js"; | ||
import { AGGREGATION_OPERATION, getValueFunc } from "../utils/aggregation-operation-utils.js"; | ||
import { getBoundingBox, getGridParams } from "../utils/grid-aggregation-utils.js"; | ||
import GridAggregationLayer from "../grid-aggregation-layer.js"; | ||
import { getAggregatorValueReader } from "./value-reader.js"; | ||
import { getBinIdRange } from "../common/utils/bounds-utils.js"; | ||
import { Matrix4 } from '@math.gl/core'; | ||
import { binOptionsUniforms } from "./bin-options-uniforms.js"; | ||
const DEFAULT_COLOR = [255, 255, 255, 255]; | ||
const DEFAULT_STROKE_WIDTH = 1; | ||
const DEFAULT_THRESHOLD = 1; | ||
const defaultProps = { | ||
// grid aggregation | ||
cellSize: { type: 'number', min: 1, max: 1000, value: 1000 }, | ||
cellSize: { type: 'number', min: 1, value: 1000 }, | ||
gridOrigin: { type: 'array', compare: true, value: [0, 0] }, | ||
getPosition: { type: 'accessor', value: (x) => x.position }, | ||
getWeight: { type: 'accessor', value: 1 }, | ||
gpuAggregation: false, // TODO(v9): Re-enable GPU aggregation. | ||
gpuAggregation: true, | ||
aggregation: 'SUM', | ||
@@ -40,3 +26,3 @@ // contour lines | ||
type: 'object', | ||
value: [{ threshold: DEFAULT_THRESHOLD }], | ||
value: [{ threshold: 1 }], | ||
optional: true, | ||
@@ -47,31 +33,59 @@ compare: 3 | ||
}; | ||
const POSITION_ATTRIBUTE_NAME = 'positions'; | ||
const DIMENSIONS = { | ||
data: { | ||
props: ['cellSize'] | ||
}, | ||
weights: { | ||
props: ['aggregation'], | ||
accessors: ['getWeight'] | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
class GridLayer extends AggregationLayer { | ||
getAggregatorType() { | ||
return this.props.gpuAggregation && WebGLAggregator.isSupported(this.context.device) | ||
? 'gpu' | ||
: 'cpu'; | ||
} | ||
}; | ||
/** Aggregate data into iso-lines or iso-bands for a given threshold and cell size. */ | ||
class ContourLayer extends GridAggregationLayer { | ||
createAggregator(type) { | ||
if (type === 'cpu') { | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({ positions }, index, opts) => { | ||
const viewport = this.state.aggregatorViewport; | ||
// project to common space | ||
const p = viewport.projectPosition(positions); | ||
const { cellSizeCommon, cellOriginCommon } = opts; | ||
return [ | ||
Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((p[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}, | ||
getValue: [{ sources: ['counts'], getValue: ({ counts }) => counts }], | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 1, | ||
bufferLayout: this.getAttributeManager().getBufferLayouts({ isInstanced: false }), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: /* glsl */ ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float counts; | ||
void getBin(out ivec2 binId) { | ||
vec3 positionCommon = project_position(positions, positions64Low); | ||
vec2 gridCoords = floor(positionCommon.xy / binOptions.cellSizeCommon); | ||
binId = ivec2(gridCoords); | ||
} | ||
void getValue(out float value) { | ||
value = counts; | ||
} | ||
` | ||
}), | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
} | ||
initializeState() { | ||
super.initializeAggregationLayer({ | ||
dimensions: DIMENSIONS | ||
}); | ||
this.setState({ | ||
contourData: {}, | ||
projectPoints: false, | ||
weights: { | ||
count: { | ||
size: 1, | ||
operation: AGGREGATION_OPERATION.SUM | ||
} | ||
} | ||
}); | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager(); | ||
attributeManager.add({ | ||
[POSITION_ATTRIBUTE_NAME]: { | ||
positions: { | ||
size: 3, | ||
@@ -82,163 +96,189 @@ accessor: 'getPosition', | ||
}, | ||
// this attribute is used in gpu aggregation path only | ||
count: { size: 3, accessor: 'getWeight' } | ||
counts: { size: 1, accessor: 'getWeight' } | ||
}); | ||
} | ||
updateState(opts) { | ||
super.updateState(opts); | ||
let contoursChanged = false; | ||
const { oldProps, props } = opts; | ||
const { aggregationDirty } = this.state; | ||
if (oldProps.contours !== props.contours || oldProps.zOffset !== props.zOffset) { | ||
contoursChanged = true; | ||
this._updateThresholdData(opts.props); | ||
updateState(params) { | ||
const aggregatorChanged = super.updateState(params); | ||
const { props, oldProps, changeFlags } = params; | ||
const { aggregator } = this.state; | ||
if (aggregatorChanged || | ||
changeFlags.dataChanged || | ||
props.cellSize !== oldProps.cellSize || | ||
!_deepEqual(props.gridOrigin, oldProps.gridOrigin, 1) || | ||
props.aggregation !== oldProps.aggregation) { | ||
this._updateBinOptions(); | ||
const { cellSizeCommon, cellOriginCommon, binIdRange } = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
pointCount: this.getNumInstances(), | ||
operations: [props.aggregation], | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
} | ||
}); | ||
} | ||
if (this.getNumInstances() > 0 && (aggregationDirty || contoursChanged)) { | ||
this._generateContours(); | ||
if (!_deepEqual(oldProps.contours, props.contours, 2)) { | ||
// Recalculate contours | ||
this.setState({ contourData: null }); | ||
} | ||
return aggregatorChanged; | ||
} | ||
_updateBinOptions() { | ||
const bounds = this.getBounds(); | ||
const cellSizeCommon = [1, 1]; | ||
let cellOriginCommon = [0, 0]; | ||
let binIdRange = [ | ||
[0, 1], | ||
[0, 1] | ||
]; | ||
let viewport = this.context.viewport; | ||
if (bounds && Number.isFinite(bounds[0][0])) { | ||
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]; | ||
const { cellSize, gridOrigin } = this.props; | ||
const { unitsPerMeter } = viewport.getDistanceScales(centroid); | ||
cellSizeCommon[0] = unitsPerMeter[0] * cellSize; | ||
cellSizeCommon[1] = unitsPerMeter[1] * cellSize; | ||
// Offset common space to center at the origin of the grid cell where the data center is in | ||
// This improves precision without affecting the cell positions | ||
const centroidCommon = viewport.projectFlat(centroid); | ||
cellOriginCommon = [ | ||
Math.floor((centroidCommon[0] - gridOrigin[0]) / cellSizeCommon[0]) * cellSizeCommon[0] + | ||
gridOrigin[0], | ||
Math.floor((centroidCommon[1] - gridOrigin[1]) / cellSizeCommon[1]) * cellSizeCommon[1] + | ||
gridOrigin[1] | ||
]; | ||
centroid = viewport.unprojectFlat(cellOriginCommon); | ||
const ViewportType = viewport.constructor; | ||
// We construct a viewport for the GPU aggregator's project module | ||
// This viewport is determined by data | ||
// removes arbitrary precision variance that depends on initial view state | ||
viewport = viewport.isGeospatial | ||
? new ViewportType({ longitude: centroid[0], latitude: centroid[1], zoom: 12 }) | ||
: new Viewport({ position: [centroid[0], centroid[1], 0], zoom: 12 }); | ||
// Round to the nearest 32-bit float to match CPU and GPU results | ||
cellOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])]; | ||
binIdRange = getBinIdRange({ | ||
dataBounds: bounds, | ||
getBinId: (p) => { | ||
const positionCommon = viewport.projectFlat(p); | ||
return [ | ||
Math.floor((positionCommon[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((positionCommon[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}); | ||
} | ||
this.setState({ cellSizeCommon, cellOriginCommon, binIdRange, aggregatorViewport: viewport }); | ||
} | ||
draw(opts) { | ||
// Replaces render time viewport with our own | ||
if (opts.shaderModuleProps.project) { | ||
opts.shaderModuleProps.project.viewport = this.state.aggregatorViewport; | ||
} | ||
super.draw(opts); | ||
} | ||
_onAggregationUpdate() { | ||
const { aggregator, binIdRange } = this.state; | ||
this.setState({ | ||
aggregatedValueReader: getAggregatorValueReader({ aggregator, binIdRange, channel: 0 }), | ||
contourData: null | ||
}); | ||
} | ||
_getContours() { | ||
const { aggregatedValueReader } = this.state; | ||
if (!aggregatedValueReader) { | ||
return null; | ||
} | ||
if (!this.state.contourData) { | ||
const { binIdRange } = this.state; | ||
const { contours } = this.props; | ||
const contourData = generateContours({ | ||
contours, | ||
getValue: aggregatedValueReader, | ||
xRange: binIdRange[0], | ||
yRange: binIdRange[1] | ||
}); | ||
this.state.contourData = contourData; | ||
} | ||
return this.state.contourData; | ||
} | ||
onAttributeChange(id) { | ||
const { aggregator } = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
this._updateBinOptions(); | ||
const { cellSizeCommon, cellOriginCommon, binIdRange } = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
} | ||
}); | ||
break; | ||
case 'counts': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
} | ||
renderLayers() { | ||
const { contourSegments, contourPolygons } = this.state.contourData; | ||
const LinesSubLayerClass = this.getSubLayerClass('lines', LineLayer); | ||
const contourData = this._getContours(); | ||
if (!contourData) { | ||
return null; | ||
} | ||
const { lines, polygons } = contourData; | ||
const { zOffset } = this.props; | ||
const { cellOriginCommon, cellSizeCommon } = this.state; | ||
const LinesSubLayerClass = this.getSubLayerClass('lines', PathLayer); | ||
const BandsSubLayerClass = this.getSubLayerClass('bands', SolidPolygonLayer); | ||
const modelMatrix = new Matrix4() | ||
.translate([cellOriginCommon[0], cellOriginCommon[1], 0]) | ||
.scale([cellSizeCommon[0], cellSizeCommon[1], zOffset]); | ||
// Contour lines layer | ||
const lineLayer = contourSegments && | ||
contourSegments.length > 0 && | ||
const lineLayer = lines && | ||
lines.length > 0 && | ||
new LinesSubLayerClass(this.getSubLayerProps({ | ||
id: 'lines' | ||
}), { | ||
data: this.state.contourData.contourSegments, | ||
getSourcePosition: d => d.start, | ||
getTargetPosition: d => d.end, | ||
getColor: d => d.contour.color || DEFAULT_COLOR, | ||
getWidth: d => d.contour.strokeWidth || DEFAULT_STROKE_WIDTH | ||
data: lines, | ||
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, | ||
modelMatrix, | ||
getPath: d => d.vertices, | ||
getColor: d => d.contour.color ?? DEFAULT_COLOR, | ||
getWidth: d => d.contour.strokeWidth ?? DEFAULT_STROKE_WIDTH, | ||
widthUnits: 'pixels' | ||
}); | ||
// Contour bands layer | ||
const bandsLayer = contourPolygons && | ||
contourPolygons.length > 0 && | ||
const bandsLayer = polygons && | ||
polygons.length > 0 && | ||
new BandsSubLayerClass(this.getSubLayerProps({ | ||
id: 'bands' | ||
}), { | ||
data: this.state.contourData.contourPolygons, | ||
data: polygons, | ||
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, | ||
modelMatrix, | ||
getPolygon: d => d.vertices, | ||
getFillColor: d => d.contour.color || DEFAULT_COLOR | ||
getFillColor: d => d.contour.color ?? DEFAULT_COLOR | ||
}); | ||
return [lineLayer, bandsLayer]; | ||
} | ||
// Aggregation Overrides | ||
/* 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.device)) { | ||
log.warn('GPU Grid Aggregation not supported, falling back to CPU')(); | ||
gpuAggregation = false; | ||
} | ||
} | ||
const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; | ||
this.setState({ | ||
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()); | ||
this.setState({ boundingBox }); | ||
} | ||
if (positionsChanged || cellSizeChanged) { | ||
const { gridOffset, translation, width, height, numCol, numRow } = getGridParams(boundingBox, cellSize, viewport, coordinateSystem); | ||
this.allocateResources(numRow, numCol); | ||
this.setState({ | ||
gridOffset, | ||
boundingBox, | ||
translation, | ||
posOffset: translation.slice(), // Used for CPU aggregation, to offset points | ||
gridOrigin: [-1 * translation[0], -1 * translation[1]], | ||
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, data } = opts.props; | ||
const { count } = this.state.weights; | ||
if (count) { | ||
count.getWeight = getWeight; | ||
count.operation = AGGREGATION_OPERATION[aggregation]; | ||
} | ||
this.setState({ getValue: getValueFunc(aggregation, getWeight, { data }) }); | ||
} | ||
_resetResults() { | ||
const { count } = this.state.weights; | ||
if (count) { | ||
count.aggregationData = null; | ||
} | ||
} | ||
// Private (Contours) | ||
_generateContours() { | ||
const { numCol, numRow, gridOrigin, gridOffset, thresholdData } = this.state; | ||
const { count } = this.state.weights; | ||
let { aggregationData } = count; | ||
if (!aggregationData) { | ||
// @ts-ignore | ||
aggregationData = count.aggregationBuffer.readSyncWebGL(); | ||
count.aggregationData = aggregationData; | ||
} | ||
const { cellWeights } = GPUGridAggregator.getCellData({ countsData: aggregationData }); | ||
const contourData = generateContours({ | ||
thresholdData, | ||
cellWeights, | ||
gridSize: [numCol, numRow], | ||
gridOrigin, | ||
cellSize: [gridOffset.xOffset, gridOffset.yOffset] | ||
}); | ||
// contourData contains both iso-lines and iso-bands if requested. | ||
this.setState({ contourData }); | ||
} | ||
_updateThresholdData(props) { | ||
const { contours, zOffset } = props; | ||
const count = contours.length; | ||
const thresholdData = new Array(count); | ||
for (let i = 0; i < count; i++) { | ||
const contour = contours[i]; | ||
thresholdData[i] = { | ||
contour, | ||
zIndex: contour.zIndex || i, | ||
zOffset | ||
getPickingInfo(params) { | ||
const info = params.info; | ||
const { object } = info; | ||
if (object) { | ||
info.object = { | ||
contour: object.contour | ||
}; | ||
} | ||
this.setState({ thresholdData }); | ||
return info; | ||
} | ||
} | ||
ContourLayer.layerName = 'ContourLayer'; | ||
ContourLayer.defaultProps = defaultProps; | ||
export default ContourLayer; | ||
GridLayer.layerName = 'ContourLayer'; | ||
GridLayer.defaultProps = defaultProps; | ||
export default GridLayer; |
@@ -1,18 +0,39 @@ | ||
export declare function generateContours({ thresholdData, cellWeights, gridSize, gridOrigin, cellSize }: { | ||
thresholdData: any; | ||
cellWeights: Float32Array; | ||
gridSize: number[]; | ||
gridOrigin: number[]; | ||
cellSize: number[]; | ||
import type { Color } from '@deck.gl/core'; | ||
export type Contour = { | ||
/** | ||
* Isolines: `threshold` value must be a single `Number`, Isolines are generated based on this threshold value. | ||
* | ||
* Isobands: `threshold` value must be an Array of two `Number`s. Isobands are generated using `[threshold[0], threshold[1])` as threshold range, i.e area that has values `>= threshold[0]` and `< threshold[1]` are rendered with corresponding color. NOTE: `threshold[0]` is inclusive and `threshold[1]` is not inclusive. | ||
*/ | ||
threshold: number | number[]; | ||
/** | ||
* RGBA color array to be used to render the contour. | ||
* @default [255, 255, 255, 255] | ||
*/ | ||
color?: Color; | ||
/** | ||
* Applicable for `Isoline`s only, width of the Isoline in pixels. | ||
* @default 1 | ||
*/ | ||
strokeWidth?: number; | ||
/** Defines z order of the contour. */ | ||
zIndex?: number; | ||
}; | ||
export type ContourLine = { | ||
vertices: number[][]; | ||
contour: Contour; | ||
}; | ||
export type ContourPolygon = { | ||
vertices: number[][]; | ||
contour: Contour; | ||
}; | ||
export declare function generateContours({ contours, getValue, xRange, yRange }: { | ||
contours: Contour[]; | ||
getValue: (x: number, y: number) => number; | ||
xRange: [number, number]; | ||
yRange: [number, number]; | ||
}): { | ||
contourSegments: { | ||
start: number[]; | ||
end: number[]; | ||
contour: any; | ||
}[]; | ||
contourPolygons: { | ||
vertices: number[][]; | ||
contour: any; | ||
}[]; | ||
lines: ContourLine[]; | ||
polygons: ContourPolygon[]; | ||
}; | ||
//# sourceMappingURL=contour-utils.d.ts.map |
@@ -1,40 +0,37 @@ | ||
import { getCode, getVertices, CONTOUR_TYPE } from "./marching-squares.js"; | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { getCode, getLines, getPolygons } from "./marching-squares.js"; | ||
// Given all the cell weights, generates contours for each threshold. | ||
/* eslint-disable max-depth */ | ||
export function generateContours({ thresholdData, cellWeights, gridSize, gridOrigin, cellSize }) { | ||
const contourSegments = []; | ||
export function generateContours({ contours, getValue, xRange, yRange }) { | ||
const contourLines = []; | ||
const contourPolygons = []; | ||
const width = gridSize[0]; | ||
const height = gridSize[1]; | ||
let segmentIndex = 0; | ||
let polygonIndex = 0; | ||
for (const data of thresholdData) { | ||
const { contour } = data; | ||
for (let i = 0; i < contours.length; i++) { | ||
const contour = contours[i]; | ||
const z = contour.zIndex ?? i; | ||
const { threshold } = contour; | ||
for (let x = -1; x < width; x++) { | ||
for (let y = -1; y < height; y++) { | ||
for (let x = xRange[0] - 1; x < xRange[1]; x++) { | ||
for (let y = yRange[0] - 1; y < yRange[1]; y++) { | ||
// Get the MarchingSquares code based on neighbor cell weights. | ||
const { code, meanCode } = getCode({ | ||
cellWeights, | ||
getValue, | ||
threshold, | ||
x, | ||
y, | ||
width, | ||
height | ||
xRange, | ||
yRange | ||
}); | ||
const opts = { | ||
type: CONTOUR_TYPE.ISO_BANDS, | ||
gridOrigin, | ||
cellSize, | ||
x, | ||
y, | ||
width, | ||
height, | ||
z, | ||
code, | ||
meanCode, | ||
thresholdData: data | ||
meanCode | ||
}; | ||
if (Array.isArray(threshold)) { | ||
opts.type = CONTOUR_TYPE.ISO_BANDS; | ||
const polygons = getVertices(opts); | ||
// ISO bands | ||
const polygons = getPolygons(opts); | ||
for (const polygon of polygons) { | ||
@@ -48,9 +45,7 @@ contourPolygons[polygonIndex++] = { | ||
else { | ||
// Get the intersection vertices based on MarchingSquares code. | ||
opts.type = CONTOUR_TYPE.ISO_LINES; | ||
const vertices = getVertices(opts); | ||
for (let i = 0; i < vertices.length; i += 2) { | ||
contourSegments[segmentIndex++] = { | ||
start: vertices[i], | ||
end: vertices[i + 1], | ||
// ISO lines | ||
const path = getLines(opts); | ||
if (path.length > 0) { | ||
contourLines[segmentIndex++] = { | ||
vertices: path, | ||
contour | ||
@@ -63,4 +58,4 @@ }; | ||
} | ||
return { contourSegments, contourPolygons }; | ||
return { lines: contourLines, polygons: contourPolygons }; | ||
} | ||
/* eslint-enable max-depth */ |
@@ -0,1 +1,4 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
// Code to Offsets Map needed to implement Marching Squares algorithm | ||
@@ -2,0 +5,0 @@ // Ref: https://en.wikipedia.org/wiki/Marching_squares |
@@ -1,10 +0,26 @@ | ||
export declare const CONTOUR_TYPE: { | ||
ISO_LINES: number; | ||
ISO_BANDS: number; | ||
export declare function getCode(opts: { | ||
getValue: (x: number, y: number) => number; | ||
threshold: number | number[]; | ||
x: number; | ||
xRange: [number, number]; | ||
y: number; | ||
yRange: [number, number]; | ||
}): { | ||
code: number; | ||
meanCode: number; | ||
}; | ||
export declare function getCode(opts: any): { | ||
export declare function getPolygons(opts: { | ||
x: number; | ||
y: number; | ||
z: number; | ||
code: number; | ||
meanCode: number; | ||
}; | ||
export declare function getVertices(opts: any): number[][] | number[][][]; | ||
}): number[][][]; | ||
export declare function getLines(opts: { | ||
x: number; | ||
y: number; | ||
z: number; | ||
code: number; | ||
meanCode: number; | ||
}): number[][]; | ||
//# sourceMappingURL=marching-squares.d.ts.map |
@@ -0,16 +1,13 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
// All utility methods needed to implement Marching Squares algorithm | ||
// Ref: https://en.wikipedia.org/wiki/Marching_squares | ||
import { log } from '@deck.gl/core'; | ||
import { ISOLINES_CODE_OFFSET_MAP, ISOBANDS_CODE_OFFSET_MAP } from "./marching-squares-codes.js"; | ||
export const CONTOUR_TYPE = { | ||
ISO_LINES: 1, | ||
ISO_BANDS: 2 | ||
}; | ||
const DEFAULT_THRESHOLD_DATA = { | ||
zIndex: 0, | ||
zOffset: 0.005 | ||
}; | ||
// Utility methods | ||
function getVertexCode(weight, threshold) { | ||
// threshold must be a single value or a range (array of size 2) | ||
if (Number.isNaN(weight)) { | ||
return 0; | ||
} | ||
// Iso-bands | ||
@@ -33,48 +30,49 @@ if (Array.isArray(threshold)) { | ||
// to create a 2X2 cell grid | ||
const { cellWeights, x, y, width, height } = opts; | ||
let threshold = opts.threshold; | ||
if (opts.thresholdValue) { | ||
log.deprecated('thresholdValue', 'threshold')(); | ||
threshold = opts.thresholdValue; | ||
} | ||
const isLeftBoundary = x < 0; | ||
const isRightBoundary = x >= width - 1; | ||
const isBottomBoundary = y < 0; | ||
const isTopBoundary = y >= height - 1; | ||
const { x, y, xRange, yRange, getValue, threshold } = opts; | ||
const isLeftBoundary = x < xRange[0]; | ||
const isRightBoundary = x >= xRange[1] - 1; | ||
const isBottomBoundary = y < yRange[0]; | ||
const isTopBoundary = y >= yRange[1] - 1; | ||
const isBoundary = isLeftBoundary || isRightBoundary || isBottomBoundary || isTopBoundary; | ||
const weights = {}; | ||
const codes = {}; | ||
let weights = 0; | ||
let current; | ||
let right; | ||
let top; | ||
let topRight; | ||
// TOP | ||
if (isLeftBoundary || isTopBoundary) { | ||
codes.top = 0; | ||
top = 0; | ||
} | ||
else { | ||
weights.top = cellWeights[(y + 1) * width + x]; | ||
codes.top = getVertexCode(weights.top, threshold); | ||
const w = getValue(x, y + 1); | ||
top = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
// TOP-RIGHT | ||
if (isRightBoundary || isTopBoundary) { | ||
codes.topRight = 0; | ||
topRight = 0; | ||
} | ||
else { | ||
weights.topRight = cellWeights[(y + 1) * width + x + 1]; | ||
codes.topRight = getVertexCode(weights.topRight, threshold); | ||
const w = getValue(x + 1, y + 1); | ||
topRight = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
// RIGHT | ||
if (isRightBoundary || isBottomBoundary) { | ||
codes.right = 0; | ||
right = 0; | ||
} | ||
else { | ||
weights.right = cellWeights[y * width + x + 1]; | ||
codes.right = getVertexCode(weights.right, threshold); | ||
const w = getValue(x + 1, y); | ||
right = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
// CURRENT | ||
if (isLeftBoundary || isBottomBoundary) { | ||
codes.current = 0; | ||
current = 0; | ||
} | ||
else { | ||
weights.current = cellWeights[y * width + x]; | ||
codes.current = getVertexCode(weights.current, threshold); | ||
const w = getValue(x, y); | ||
current = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
const { top, topRight, right, current } = codes; | ||
let code = -1; | ||
@@ -92,3 +90,3 @@ if (Number.isFinite(threshold)) { | ||
if (!isBoundary) { | ||
meanCode = getVertexCode((weights.top + weights.topRight + weights.right + weights.current) / 4, threshold); | ||
meanCode = getVertexCode(weights / 4, threshold); | ||
} | ||
@@ -100,8 +98,5 @@ return { code, meanCode }; | ||
// [x, y] refers current marching cell, reference vertex is always top-right corner | ||
export function getVertices(opts) { | ||
const { gridOrigin, cellSize, x, y, code, meanCode, type = CONTOUR_TYPE.ISO_LINES } = opts; | ||
const thresholdData = { ...DEFAULT_THRESHOLD_DATA, ...opts.thresholdData }; | ||
let offsets = type === CONTOUR_TYPE.ISO_BANDS | ||
? ISOBANDS_CODE_OFFSET_MAP[code] | ||
: ISOLINES_CODE_OFFSET_MAP[code]; | ||
export function getPolygons(opts) { | ||
const { x, y, z, code, meanCode } = opts; | ||
let offsets = ISOBANDS_CODE_OFFSET_MAP[code]; | ||
// handle saddle cases | ||
@@ -112,37 +107,45 @@ if (!Array.isArray(offsets)) { | ||
// Reference vertex is at top-right move to top-right corner | ||
const vZ = thresholdData.zIndex * thresholdData.zOffset; | ||
const rX = (x + 1) * cellSize[0]; | ||
const rY = (y + 1) * cellSize[1]; | ||
const refVertexX = gridOrigin[0] + rX; | ||
const refVertexY = gridOrigin[1] + rY; | ||
const rX = x + 1; | ||
const rY = y + 1; | ||
// offsets format | ||
// ISO_LINES: [[1A, 1B], [2A, 2B]], | ||
// ISO_BANDS: [[1A, 1B, 1C, ...], [2A, 2B, 2C, ...]], | ||
// [[1A, 1B, 1C, ...], [2A, 2B, 2C, ...]], | ||
// vertices format | ||
// ISO_LINES: [[x1A, y1A], [x1B, y1B], [x2A, x2B], ...], | ||
// ISO_BANDS: => confirms to SolidPolygonLayer's simple polygon format | ||
// [ | ||
// [[x1A, y1A], [x1B, y1B], [x1C, y1C] ... ], | ||
// [ | ||
// [[x1A, y1A], [x1B, y1B], [x1C, y1C] ... ], | ||
// ... | ||
// ] | ||
if (type === CONTOUR_TYPE.ISO_BANDS) { | ||
const polygons = []; | ||
offsets.forEach(polygonOffsets => { | ||
const polygon = []; | ||
polygonOffsets.forEach(xyOffset => { | ||
const vX = refVertexX + xyOffset[0] * cellSize[0]; | ||
const vY = refVertexY + xyOffset[1] * cellSize[1]; | ||
polygon.push([vX, vY, vZ]); | ||
}); | ||
polygons.push(polygon); | ||
// ] | ||
const polygons = []; | ||
offsets.forEach(polygonOffsets => { | ||
const polygon = []; | ||
polygonOffsets.forEach(xyOffset => { | ||
const vX = rX + xyOffset[0]; | ||
const vY = rY + xyOffset[1]; | ||
polygon.push([vX, vY, z]); | ||
}); | ||
return polygons; | ||
polygons.push(polygon); | ||
}); | ||
return polygons; | ||
} | ||
// Returns intersection vertices for given cellindex | ||
// [x, y] refers current marching cell, reference vertex is always top-right corner | ||
export function getLines(opts) { | ||
const { x, y, z, code, meanCode } = opts; | ||
let offsets = ISOLINES_CODE_OFFSET_MAP[code]; | ||
// handle saddle cases | ||
if (!Array.isArray(offsets)) { | ||
offsets = offsets[meanCode]; | ||
} | ||
// default case is ISO_LINES | ||
// Reference vertex is at top-right move to top-right corner | ||
const rX = x + 1; | ||
const rY = y + 1; | ||
// offsets format | ||
// [[1A, 1B], [2A, 2B]], | ||
// vertices format | ||
// [[x1A, y1A], [x1B, y1B], [x2A, x2B], ...], | ||
const lines = []; | ||
offsets.forEach(xyOffsets => { | ||
xyOffsets.forEach(offset => { | ||
const vX = refVertexX + offset[0] * cellSize[0]; | ||
const vY = refVertexY + offset[1] * cellSize[1]; | ||
lines.push([vX, vY, vZ]); | ||
const vX = rX + offset[0]; | ||
const vY = rY + offset[1]; | ||
lines.push([vX, vY, z]); | ||
}); | ||
@@ -149,0 +152,0 @@ }); |
@@ -1,14 +0,150 @@ | ||
import { CompositeLayer, CompositeLayerProps, Layer, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import { GPUGridLayerProps } from "../gpu-grid-layer/gpu-grid-layer.js"; | ||
import { CPUGridLayerProps } from "../cpu-grid-layer/cpu-grid-layer.js"; | ||
import { Accessor, Color, GetPickingInfoParams, CompositeLayerProps, Layer, Material, LayersList, PickingInfo, Position, Viewport, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator, AggregationOperation } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import { AggregateAccessor } from "../common/types.js"; | ||
import { AttributeWithScale } from "../common/utils/scale-utils.js"; | ||
import { BinOptions } from "./bin-options-uniforms.js"; | ||
/** All properties supported by GridLayer. */ | ||
export type GridLayerProps<DataT = unknown> = _GridLayerProps<DataT> & CompositeLayerProps; | ||
/** Properties added by GridLayer. */ | ||
type _GridLayerProps<DataT> = CPUGridLayerProps<DataT> & GPUGridLayerProps<DataT> & { | ||
type _GridLayerProps<DataT> = { | ||
/** | ||
* Whether the aggregation should be performed in high-precision 64-bit mode. | ||
* @default false | ||
* Custom accessor to retrieve a grid bin index from each data object. | ||
* Not supported by GPU aggregation. | ||
*/ | ||
fp64?: boolean; | ||
gridAggregator?: ((position: number[], cellSize: number) => [number, number]) | null; | ||
/** | ||
* Size of each cell in meters. | ||
* @default 1000 | ||
*/ | ||
cellSize?: number; | ||
/** | ||
* Color scale domain, default is set to the extent of aggregated weights in each cell. | ||
* @default [min(colorWeight), max(colorWeight)] | ||
*/ | ||
colorDomain?: [number, number] | null; | ||
/** | ||
* Default: [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) `6-class YlOrRd` | ||
*/ | ||
colorRange?: Color[]; | ||
/** | ||
* Cell size multiplier, clamped between 0 - 1. | ||
* @default 1 | ||
*/ | ||
coverage?: number; | ||
/** | ||
* Elevation scale input domain, default is set to between 0 and the max of aggregated weights in each cell. | ||
* @default [0, max(elevationWeight)] | ||
*/ | ||
elevationDomain?: [number, number] | null; | ||
/** | ||
* Elevation scale output range. | ||
* @default [0, 1000] | ||
*/ | ||
elevationRange?: [number, number]; | ||
/** | ||
* Cell elevation multiplier. | ||
* @default 1 | ||
*/ | ||
elevationScale?: number; | ||
/** | ||
* Whether to enable cell elevation. If set to false, all cell will be flat. | ||
* @default true | ||
*/ | ||
extruded?: boolean; | ||
/** | ||
* Filter cells and re-calculate color by `upperPercentile`. | ||
* Cells with value larger than the upperPercentile will be hidden. | ||
* @default 100 | ||
*/ | ||
upperPercentile?: number; | ||
/** | ||
* Filter cells and re-calculate color by `lowerPercentile`. | ||
* Cells with value smaller than the lowerPercentile will be hidden. | ||
* @default 0 | ||
*/ | ||
lowerPercentile?: number; | ||
/** | ||
* Filter cells and re-calculate elevation by `elevationUpperPercentile`. | ||
* Cells with elevation value larger than the `elevationUpperPercentile` will be hidden. | ||
* @default 100 | ||
*/ | ||
elevationUpperPercentile?: number; | ||
/** | ||
* Filter cells and re-calculate elevation by `elevationLowerPercentile`. | ||
* Cells with elevation value larger than the `elevationLowerPercentile` will be hidden. | ||
* @default 0 | ||
*/ | ||
elevationLowerPercentile?: number; | ||
/** | ||
* Scaling function used to determine the color of the grid cell. | ||
* Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'. | ||
* @default 'quantize' | ||
*/ | ||
colorScaleType?: 'quantize' | 'linear' | 'quantile' | 'ordinal'; | ||
/** | ||
* Scaling function used to determine the elevation of the grid cell. | ||
* Supported Values are 'linear' and 'quantile'. | ||
* @default 'linear' | ||
*/ | ||
elevationScaleType?: 'linear' | 'quantile'; | ||
/** | ||
* Material settings for lighting effect. Applies if `extruded: true`. | ||
* | ||
* @default true | ||
* @see https://deck.gl/docs/developer-guide/using-lighting | ||
*/ | ||
material?: Material; | ||
/** | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's color value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
colorAggregation?: AggregationOperation; | ||
/** | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's elevation value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
elevationAggregation?: AggregationOperation; | ||
/** | ||
* Method called to retrieve the position of each object. | ||
* @default object => object.position | ||
*/ | ||
getPosition?: Accessor<DataT, Position>; | ||
/** | ||
* The weight of a data object used to calculate the color value for a cell. | ||
* @default 1 | ||
*/ | ||
getColorWeight?: Accessor<DataT, number>; | ||
/** | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its color is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
*/ | ||
getColorValue?: AggregateAccessor<DataT> | null; | ||
/** | ||
* The weight of a data object used to calculate the elevation value for a cell. | ||
* @default 1 | ||
*/ | ||
getElevationWeight?: Accessor<DataT, number>; | ||
/** | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its elevation is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
*/ | ||
getElevationValue?: AggregateAccessor<DataT> | null; | ||
/** | ||
* This callback will be called when bin color domain has been calculated. | ||
* @default () => {} | ||
*/ | ||
onSetColorDomain?: (minMax: [number, number]) => void; | ||
/** | ||
* This callback will be called when bin elevation domain has been calculated. | ||
* @default () => {} | ||
*/ | ||
onSetElevationDomain?: (minMax: [number, number]) => void; | ||
/** | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
@@ -19,15 +155,41 @@ * @default false | ||
}; | ||
export type GridLayerPickingInfo<DataT> = PickingInfo<{ | ||
/** Column index of the picked cell */ | ||
col: number; | ||
/** Row index of the picked cell */ | ||
row: number; | ||
/** Aggregated color value, as determined by `getColorWeight` and `colorAggregation` */ | ||
colorValue: number; | ||
/** Aggregated elevation value, as determined by `getElevationWeight` and `elevationAggregation` */ | ||
elevationValue: number; | ||
/** Number of data points in the picked cell */ | ||
count: number; | ||
/** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */ | ||
pointIndices?: number[]; | ||
/** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */ | ||
points?: DataT[]; | ||
}>; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends CompositeLayer<ExtraPropsT & Required<_GridLayerProps<DataT>>> { | ||
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends AggregationLayer<DataT, ExtraPropsT & Required<_GridLayerProps<DataT>>> { | ||
static layerName: string; | ||
static defaultProps: DefaultProps<GridLayerProps<unknown>>; | ||
state: CompositeLayer['state'] & { | ||
useGPUAggregation: boolean; | ||
state: AggregationLayer<DataT>['state'] & BinOptions & { | ||
dataAsArray?: DataT[]; | ||
colors?: AttributeWithScale; | ||
elevations?: AttributeWithScale; | ||
binIdRange: [number, number][]; | ||
aggregatorViewport: Viewport; | ||
}; | ||
getAggregatorType(): string; | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator; | ||
initializeState(): void; | ||
updateState({ props }: UpdateParameters<this>): void; | ||
renderLayers(): Layer; | ||
canUseGPUAggregation(props: GridLayer['props']): boolean; | ||
updateState(params: UpdateParameters<this>): boolean; | ||
private _updateBinOptions; | ||
draw(opts: any): void; | ||
private _onAggregationUpdate; | ||
onAttributeChange(id: string): void; | ||
renderLayers(): LayersList | Layer | null; | ||
getPickingInfo(params: GetPickingInfoParams): GridLayerPickingInfo<DataT>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=grid-layer.d.ts.map |
@@ -1,61 +0,357 @@ | ||
import { CompositeLayer } from '@deck.gl/core'; | ||
import GPUGridAggregator from "../utils/gpu-grid-aggregation/gpu-grid-aggregator.js"; | ||
import GPUGridLayer from "../gpu-grid-layer/gpu-grid-layer.js"; | ||
import CPUGridLayer from "../cpu-grid-layer/cpu-grid-layer.js"; | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { log, createIterable, project32, Viewport } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import { defaultColorRange } from "../common/utils/color-utils.js"; | ||
import { AttributeWithScale } from "../common/utils/scale-utils.js"; | ||
import { getBinIdRange } from "../common/utils/bounds-utils.js"; | ||
import { GridCellLayer } from "./grid-cell-layer.js"; | ||
import { binOptionsUniforms } from "./bin-options-uniforms.js"; | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
function noop() { } | ||
const defaultProps = { | ||
...GPUGridLayer.defaultProps, | ||
...CPUGridLayer.defaultProps, | ||
gpuAggregation: false | ||
gpuAggregation: false, | ||
// color | ||
colorDomain: null, | ||
colorRange: defaultColorRange, | ||
getColorValue: { type: 'accessor', value: null }, // default value is calculated from `getColorWeight` and `colorAggregation` | ||
getColorWeight: { type: 'accessor', value: 1 }, | ||
colorAggregation: 'SUM', | ||
lowerPercentile: { type: 'number', min: 0, max: 100, value: 0 }, | ||
upperPercentile: { type: 'number', min: 0, max: 100, value: 100 }, | ||
colorScaleType: 'quantize', | ||
onSetColorDomain: noop, | ||
// elevation | ||
elevationDomain: null, | ||
elevationRange: [0, 1000], | ||
getElevationValue: { type: 'accessor', value: null }, // default value is calculated from `getElevationWeight` and `elevationAggregation` | ||
getElevationWeight: { type: 'accessor', value: 1 }, | ||
elevationAggregation: 'SUM', | ||
elevationScale: { type: 'number', min: 0, value: 1 }, | ||
elevationLowerPercentile: { type: 'number', min: 0, max: 100, value: 0 }, | ||
elevationUpperPercentile: { type: 'number', min: 0, max: 100, value: 100 }, | ||
elevationScaleType: 'linear', | ||
onSetElevationDomain: noop, | ||
// grid | ||
cellSize: { type: 'number', min: 0, value: 1000 }, | ||
coverage: { type: 'number', min: 0, max: 1, value: 1 }, | ||
getPosition: { type: 'accessor', value: (x) => x.position }, | ||
gridAggregator: { type: 'function', optional: true, value: null }, | ||
extruded: false, | ||
// Optional material for 'lighting' shader module | ||
material: true | ||
}; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
class GridLayer extends CompositeLayer { | ||
initializeState() { | ||
this.state = { | ||
useGPUAggregation: false // TODO(v9): Re-enable GPU aggregation. | ||
}; | ||
class GridLayer extends AggregationLayer { | ||
getAggregatorType() { | ||
const { gpuAggregation, gridAggregator, getColorValue, getElevationValue } = this.props; | ||
if (gpuAggregation && (gridAggregator || getColorValue || getElevationValue)) { | ||
// If these features are desired by the app, the user should explicitly use CPU aggregation | ||
log.warn('Features not supported by GPU aggregation, falling back to CPU')(); | ||
return 'cpu'; | ||
} | ||
if ( | ||
// GPU aggregation is requested | ||
gpuAggregation && | ||
// GPU aggregation is supported by the device | ||
WebGLAggregator.isSupported(this.context.device)) { | ||
return 'gpu'; | ||
} | ||
return 'cpu'; | ||
} | ||
updateState({ props }) { | ||
this.setState({ | ||
// TODO(v9): Re-enable GPU aggregation. | ||
// useGPUAggregation: this.canUseGPUAggregation(props) | ||
useGPUAggregation: false | ||
createAggregator(type) { | ||
if (type === 'cpu') { | ||
const { gridAggregator, cellSize } = this.props; | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({ positions }, index, opts) => { | ||
if (gridAggregator) { | ||
return gridAggregator(positions, cellSize); | ||
} | ||
const viewport = this.state.aggregatorViewport; | ||
// project to common space | ||
const p = viewport.projectPosition(positions); | ||
const { cellSizeCommon, cellOriginCommon } = opts; | ||
return [ | ||
Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((p[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}, | ||
getValue: [ | ||
{ sources: ['colorWeights'], getValue: ({ colorWeights }) => colorWeights }, | ||
{ sources: ['elevationWeights'], getValue: ({ elevationWeights }) => elevationWeights } | ||
] | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 2, | ||
bufferLayout: this.getAttributeManager().getBufferLayouts({ isInstanced: false }), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: /* glsl */ ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float colorWeights; | ||
in float elevationWeights; | ||
void getBin(out ivec2 binId) { | ||
vec3 positionCommon = project_position(positions, positions64Low); | ||
vec2 gridCoords = floor(positionCommon.xy / binOptions.cellSizeCommon); | ||
binId = ivec2(gridCoords); | ||
} | ||
void getValue(out vec2 value) { | ||
value = vec2(colorWeights, elevationWeights); | ||
} | ||
` | ||
}) | ||
}); | ||
} | ||
renderLayers() { | ||
const { data, updateTriggers } = this.props; | ||
const id = this.state.useGPUAggregation ? 'GPU' : 'CPU'; | ||
const LayerType = this.state.useGPUAggregation | ||
? this.getSubLayerClass('GPU', GPUGridLayer) | ||
: this.getSubLayerClass('CPU', CPUGridLayer); | ||
return new LayerType(this.props, this.getSubLayerProps({ | ||
id, | ||
updateTriggers | ||
}), { | ||
data | ||
initializeState() { | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager(); | ||
attributeManager.add({ | ||
positions: { | ||
size: 3, | ||
accessor: 'getPosition', | ||
type: 'float64', | ||
fp64: this.use64bitPositions() | ||
}, | ||
colorWeights: { size: 1, accessor: 'getColorWeight' }, | ||
elevationWeights: { size: 1, accessor: 'getElevationWeight' } | ||
}); | ||
} | ||
// Private methods | ||
canUseGPUAggregation(props) { | ||
const { gpuAggregation, lowerPercentile, upperPercentile, getColorValue, getElevationValue, colorScaleType } = props; | ||
if (!gpuAggregation) { | ||
// cpu aggregation is requested | ||
return false; | ||
updateState(params) { | ||
const aggregatorChanged = super.updateState(params); | ||
const { props, oldProps, changeFlags } = params; | ||
const { aggregator } = this.state; | ||
if ((changeFlags.dataChanged || !this.state.dataAsArray) && | ||
(props.getColorValue || props.getElevationValue)) { | ||
// Convert data to array | ||
this.state.dataAsArray = Array.from(createIterable(props.data).iterable); | ||
} | ||
if (!GPUGridAggregator.isSupported(this.context.device)) { | ||
return false; | ||
if (aggregatorChanged || | ||
changeFlags.dataChanged || | ||
props.cellSize !== oldProps.cellSize || | ||
props.getColorValue !== oldProps.getColorValue || | ||
props.getElevationValue !== oldProps.getElevationValue || | ||
props.colorAggregation !== oldProps.colorAggregation || | ||
props.elevationAggregation !== oldProps.elevationAggregation) { | ||
this._updateBinOptions(); | ||
const { cellSizeCommon, cellOriginCommon, binIdRange, dataAsArray } = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
pointCount: this.getNumInstances(), | ||
operations: [props.colorAggregation, props.elevationAggregation], | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
}, | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
if (dataAsArray) { | ||
const { getColorValue, getElevationValue } = this.props; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by CPUAggregator | ||
customOperations: [ | ||
getColorValue && | ||
((indices) => getColorValue(indices.map(i => dataAsArray[i]), { indices, data: props.data })), | ||
getElevationValue && | ||
((indices) => getElevationValue(indices.map(i => dataAsArray[i]), { indices, data: props.data })) | ||
] | ||
}); | ||
} | ||
} | ||
if (lowerPercentile !== 0 || upperPercentile !== 100) { | ||
// percentile calculations requires sorting not supported on GPU | ||
return false; | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getColorValue) { | ||
aggregator.setNeedsUpdate(0); | ||
} | ||
if (getColorValue !== null || getElevationValue !== null) { | ||
// accessor for custom color or elevation calculation is specified | ||
return false; | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getElevationValue) { | ||
aggregator.setNeedsUpdate(1); | ||
} | ||
if (colorScaleType === 'quantile' || colorScaleType === 'ordinal') { | ||
// quantile and ordinal scales are not supported on GPU | ||
return false; | ||
return aggregatorChanged; | ||
} | ||
_updateBinOptions() { | ||
const bounds = this.getBounds(); | ||
const cellSizeCommon = [1, 1]; | ||
let cellOriginCommon = [0, 0]; | ||
let binIdRange = [ | ||
[0, 1], | ||
[0, 1] | ||
]; | ||
let viewport = this.context.viewport; | ||
if (bounds && Number.isFinite(bounds[0][0])) { | ||
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]; | ||
const { cellSize } = this.props; | ||
const { unitsPerMeter } = viewport.getDistanceScales(centroid); | ||
cellSizeCommon[0] = unitsPerMeter[0] * cellSize; | ||
cellSizeCommon[1] = unitsPerMeter[1] * cellSize; | ||
// Offset common space to center at the origin of the grid cell where the data center is in | ||
// This improves precision without affecting the cell positions | ||
const centroidCommon = viewport.projectFlat(centroid); | ||
cellOriginCommon = [ | ||
Math.floor(centroidCommon[0] / cellSizeCommon[0]) * cellSizeCommon[0], | ||
Math.floor(centroidCommon[1] / cellSizeCommon[1]) * cellSizeCommon[1] | ||
]; | ||
centroid = viewport.unprojectFlat(cellOriginCommon); | ||
const ViewportType = viewport.constructor; | ||
// We construct a viewport for the GPU aggregator's project module | ||
// This viewport is determined by data | ||
// removes arbitrary precision variance that depends on initial view state | ||
viewport = viewport.isGeospatial | ||
? new ViewportType({ longitude: centroid[0], latitude: centroid[1], zoom: 12 }) | ||
: new Viewport({ position: [centroid[0], centroid[1], 0], zoom: 12 }); | ||
// Round to the nearest 32-bit float to match CPU and GPU results | ||
cellOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])]; | ||
binIdRange = getBinIdRange({ | ||
dataBounds: bounds, | ||
getBinId: (p) => { | ||
const positionCommon = viewport.projectFlat(p); | ||
return [ | ||
Math.floor((positionCommon[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((positionCommon[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}); | ||
} | ||
return true; | ||
this.setState({ cellSizeCommon, cellOriginCommon, binIdRange, aggregatorViewport: viewport }); | ||
} | ||
draw(opts) { | ||
// Replaces render time viewport with our own | ||
if (opts.shaderModuleProps.project) { | ||
opts.shaderModuleProps.project.viewport = this.state.aggregatorViewport; | ||
} | ||
super.draw(opts); | ||
} | ||
_onAggregationUpdate({ channel }) { | ||
const props = this.getCurrentLayer().props; | ||
const { aggregator } = this.state; | ||
if (channel === 0) { | ||
const result = aggregator.getResult(0); | ||
this.setState({ | ||
colors: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetColorDomain(aggregator.getResultDomain(0)); | ||
} | ||
else if (channel === 1) { | ||
const result = aggregator.getResult(1); | ||
this.setState({ | ||
elevations: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetElevationDomain(aggregator.getResultDomain(1)); | ||
} | ||
} | ||
onAttributeChange(id) { | ||
const { aggregator } = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
this._updateBinOptions(); | ||
const { cellSizeCommon, cellOriginCommon, binIdRange } = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
} | ||
}); | ||
break; | ||
case 'colorWeights': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
case 'elevationWeights': | ||
aggregator.setNeedsUpdate(1); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
} | ||
renderLayers() { | ||
const { aggregator, cellOriginCommon, cellSizeCommon } = this.state; | ||
const { elevationScale, colorRange, elevationRange, extruded, coverage, material, transitions, colorScaleType, lowerPercentile, upperPercentile, colorDomain, elevationScaleType, elevationLowerPercentile, elevationUpperPercentile, elevationDomain } = this.props; | ||
const CellLayerClass = this.getSubLayerClass('cells', GridCellLayer); | ||
const binAttribute = aggregator.getBins(); | ||
const colors = this.state.colors?.update({ | ||
scaleType: colorScaleType, | ||
lowerPercentile, | ||
upperPercentile | ||
}); | ||
const elevations = this.state.elevations?.update({ | ||
scaleType: elevationScaleType, | ||
lowerPercentile: elevationLowerPercentile, | ||
upperPercentile: elevationUpperPercentile | ||
}); | ||
if (!colors || !elevations) { | ||
return null; | ||
} | ||
return new CellLayerClass(this.getSubLayerProps({ | ||
id: 'cells' | ||
}), { | ||
data: { | ||
length: aggregator.binCount, | ||
attributes: { | ||
getBin: binAttribute, | ||
getColorValue: colors.attribute, | ||
getElevationValue: elevations.attribute | ||
} | ||
}, | ||
// Data has changed shallowly, but we likely don't need to update the attributes | ||
dataComparator: (data, oldData) => data.length === oldData.length, | ||
updateTriggers: { | ||
getBin: [binAttribute], | ||
getColorValue: [colors.attribute], | ||
getElevationValue: [elevations.attribute] | ||
}, | ||
cellOriginCommon, | ||
cellSizeCommon, | ||
elevationScale, | ||
colorRange, | ||
colorScaleType, | ||
elevationRange, | ||
extruded, | ||
coverage, | ||
material, | ||
colorDomain: colors.domain || colorDomain || aggregator.getResultDomain(0), | ||
elevationDomain: elevations.domain || elevationDomain || aggregator.getResultDomain(1), | ||
colorCutoff: colors.cutoff, | ||
elevationCutoff: elevations.cutoff, | ||
transitions: transitions && { | ||
getFillColor: transitions.getColorValue || transitions.getColorWeight, | ||
getElevation: transitions.getElevationValue || transitions.getElevationWeight | ||
}, | ||
// Extensions are already handled by the GPUAggregator, do not pass it down | ||
extensions: [] | ||
}); | ||
} | ||
getPickingInfo(params) { | ||
const info = params.info; | ||
const { index } = info; | ||
if (index >= 0) { | ||
const bin = this.state.aggregator.getBin(index); | ||
let object; | ||
if (bin) { | ||
object = { | ||
col: bin.id[0], | ||
row: bin.id[1], | ||
colorValue: bin.value[0], | ||
elevationValue: bin.value[1], | ||
count: bin.count | ||
}; | ||
if (bin.pointIndices) { | ||
object.pointIndices = bin.pointIndices; | ||
object.points = Array.isArray(this.props.data) | ||
? bin.pointIndices.map(i => this.props.data[i]) | ||
: []; | ||
} | ||
} | ||
info.object = object; | ||
} | ||
return info; | ||
} | ||
} | ||
@@ -62,0 +358,0 @@ GridLayer.layerName = 'GridLayer'; |
export declare function getBounds(points: number[][]): number[]; | ||
export declare function boundsContain(currentBounds: number[], targetBounds: number[]): boolean; | ||
export declare function packVertices(points: number[][], dimensions?: number): Float32Array; | ||
export declare function scaleToAspectRatio(boundingBox: number[], width: number, height: number): number[]; | ||
export declare function scaleToAspectRatio(boundingBox: [number, number, number, number], width: number, height: number): [number, number, number, number]; | ||
export declare function getTextureCoordinates(point: number[], bounds: number[]): number[]; | ||
//# sourceMappingURL=heatmap-layer-utils.d.ts.map |
@@ -0,1 +1,4 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export function getBounds(points) { | ||
@@ -2,0 +5,0 @@ // Now build bounding box in world space (aligned to world coordiante system) |
import { Buffer, Texture, TextureFormat } from '@luma.gl/core'; | ||
import { TextureTransform } from '@luma.gl/engine'; | ||
import { Accessor, AccessorFunction, AttributeManager, ChangeFlags, Color, Layer, LayerContext, LayersList, Position, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import AggregationLayer, { AggregationLayerProps } from "../aggregation-layer.js"; | ||
import AggregationLayer, { AggregationLayerProps } from "./aggregation-layer.js"; | ||
export type HeatmapLayerProps<DataT = unknown> = _HeatmapLayerProps<DataT> & AggregationLayerProps<DataT>; | ||
@@ -96,2 +96,3 @@ type _HeatmapLayerProps<DataT> = { | ||
}; | ||
getShaders(shaders: any): any; | ||
initializeState(): void; | ||
@@ -105,4 +106,4 @@ shouldUpdateState({ changeFlags }: UpdateParameters<this>): boolean; | ||
_getChangeFlags(opts: UpdateParameters<this>): Partial<ChangeFlags> & { | ||
boundsChanged?: boolean | undefined; | ||
viewportZoomChanged?: boolean | undefined; | ||
boundsChanged?: boolean; | ||
viewportZoomChanged?: boolean; | ||
}; | ||
@@ -115,2 +116,3 @@ _createTextures(): void; | ||
fs?: string; | ||
modules: any[]; | ||
}): void; | ||
@@ -127,3 +129,3 @@ _setupResources(): void; | ||
useLayerCoordinateSystem?: boolean; | ||
}): number[]; | ||
}): [number, number, number, number]; | ||
_commonToWorldBounds(commonBounds: any): number[]; | ||
@@ -130,0 +132,0 @@ } |
@@ -1,27 +0,11 @@ | ||
// Copyright (c) 2015 - 2019 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
/* global setTimeout clearTimeout */ | ||
import { getBounds, boundsContain, packVertices, scaleToAspectRatio, getTextureCoordinates } from "./heatmap-layer-utils.js"; | ||
import { TextureTransform } from '@luma.gl/engine'; | ||
import { AttributeManager, COORDINATE_SYSTEM, log } from '@deck.gl/core'; | ||
import { AttributeManager, COORDINATE_SYSTEM, log, project32 } from '@deck.gl/core'; | ||
import TriangleLayer from "./triangle-layer.js"; | ||
import AggregationLayer from "../aggregation-layer.js"; | ||
import { defaultColorRange, colorRangeToFlatArray } from "../utils/color-utils.js"; | ||
import AggregationLayer from "./aggregation-layer.js"; | ||
import { defaultColorRange, colorRangeToFlatArray } from "../common/utils/color-utils.js"; | ||
import weightsVs from "./weights-vs.glsl.js"; | ||
@@ -31,2 +15,3 @@ import weightsFs from "./weights-fs.glsl.js"; | ||
import maxFs from "./max-fs.glsl.js"; | ||
import { maxWeightUniforms, weightUniforms } from "./heatmap-layer-uniforms.js"; | ||
const RESOLUTION = 2; // (number of common space pixels) / (number texels) | ||
@@ -72,2 +57,9 @@ const TEXTURE_PROPS = { | ||
class HeatmapLayer extends AggregationLayer { | ||
getShaders(shaders) { | ||
let modules = [project32]; | ||
if (shaders.modules) { | ||
modules = [...modules, ...shaders.modules]; | ||
} | ||
return super.getShaders({ ...shaders, modules }); | ||
} | ||
initializeState() { | ||
@@ -236,3 +228,4 @@ super.initializeAggregationLayer(DIMENSIONS); | ||
topology: 'point-list', | ||
...shaders | ||
...shaders, | ||
modules: [...shaders.modules, weightUniforms] | ||
}); | ||
@@ -250,7 +243,9 @@ this.setState({ weightsTransform }); | ||
this._createWeightsTransform(weightsTransformShaders); | ||
const maxWeightsTransformShaders = this.getShaders({ vs: maxVs, fs: maxFs }); | ||
const maxWeightsTransformShaders = this.getShaders({ | ||
vs: maxVs, | ||
fs: maxFs, | ||
modules: [maxWeightUniforms] | ||
}); | ||
const maxWeightTransform = new TextureTransform(device, { | ||
id: `${this.id}-max-weights-transform`, | ||
bindings: { inTexture: weightsTexture }, | ||
uniforms: { textureSize }, | ||
targetTexture: maxWeightsTexture, | ||
@@ -270,2 +265,6 @@ ...maxWeightsTransformShaders, | ||
}); | ||
const maxWeightProps = { inTexture: weightsTexture, textureSize }; | ||
maxWeightTransform.model.shaderInputs.setProps({ | ||
maxWeight: maxWeightProps | ||
}); | ||
this.setState({ | ||
@@ -304,4 +303,4 @@ weightsTexture, | ||
viewport.unproject([viewport.width, 0]), | ||
viewport.unproject([viewport.width, viewport.height]), | ||
viewport.unproject([0, viewport.height]) | ||
viewport.unproject([0, viewport.height]), | ||
viewport.unproject([viewport.width, viewport.height]) | ||
].map(p => p.map(Math.fround)); | ||
@@ -350,6 +349,7 @@ // #1: get world bounds for current viewport extends | ||
// TODO(v9): Unclear whether `setSubImageData` is a public API, or what to use if not. | ||
colorTexture.setSubImageData({ data: colors }); | ||
colorTexture.setTexture2DData({ data: colors }); | ||
} | ||
else { | ||
colorTexture?.destroy(); | ||
// @ts-expect-error TODO(ib) - texture API change | ||
colorTexture = this.context.device.createTexture({ | ||
@@ -366,3 +366,3 @@ ...TEXTURE_PROPS, | ||
const { radiusPixels, colorDomain, aggregation } = this.props; | ||
const { worldBounds, textureSize, weightsScale } = this.state; | ||
const { worldBounds, textureSize, weightsScale, weightsTexture } = this.state; | ||
const weightsTransform = this.state.weightsTransform; | ||
@@ -387,7 +387,17 @@ this.state.isWeightMapDirty = false; | ||
const moduleSettings = this.getModuleSettings(); | ||
const uniforms = { radiusPixels, commonBounds, textureWidth: textureSize, weightsScale }; | ||
this._setModelAttributes(weightsTransform.model, attributes); | ||
weightsTransform.model.setVertexCount(this.getNumInstances()); | ||
weightsTransform.model.setUniforms(uniforms); | ||
weightsTransform.model.updateModuleSettings(moduleSettings); | ||
const weightProps = { | ||
radiusPixels, | ||
commonBounds, | ||
textureWidth: textureSize, | ||
weightsScale, | ||
weightsTexture: weightsTexture | ||
}; | ||
const { viewport, devicePixelRatio, coordinateSystem, coordinateOrigin } = moduleSettings; | ||
const { modelMatrix } = this.props; | ||
weightsTransform.model.shaderInputs.setProps({ | ||
project: { viewport, devicePixelRatio, modelMatrix, coordinateSystem, coordinateOrigin }, | ||
weight: weightProps | ||
}); | ||
weightsTransform.run({ | ||
@@ -394,0 +404,0 @@ parameters: { viewport: [0, 0, textureSize, textureSize] }, |
@@ -0,1 +1,4 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -2,0 +5,0 @@ #version 300 es |
@@ -1,3 +0,3 @@ | ||
declare const _default: "#version 300 es\nuniform sampler2D inTexture;\nuniform float textureSize;\nout vec4 outTexture;\n\nvoid main()\n{\n // Sample every pixel in texture\n int yIndex = gl_VertexID / int(textureSize);\n int xIndex = gl_VertexID - (yIndex * int(textureSize));\n vec2 uv = (0.5 + vec2(float(xIndex), float(yIndex))) / textureSize;\n outTexture = texture(inTexture, uv);\n\n gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n // Enforce default value for ANGLE issue (https://bugs.chromium.org/p/angleproject/issues/detail?id=3941)\n gl_PointSize = 1.0;\n}\n"; | ||
declare const _default: "#version 300 es\nuniform sampler2D inTexture;\nout vec4 outTexture;\n\nvoid main()\n{\n // Sample every pixel in texture\n int yIndex = gl_VertexID / int(maxWeight.textureSize);\n int xIndex = gl_VertexID - (yIndex * int(maxWeight.textureSize));\n vec2 uv = (0.5 + vec2(float(xIndex), float(yIndex))) / maxWeight.textureSize;\n outTexture = texture(inTexture, uv);\n\n gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n // Enforce default value for ANGLE issue (https://bugs.chromium.org/p/angleproject/issues/detail?id=3941)\n gl_PointSize = 1.0;\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=max-vs.glsl.d.ts.map |
@@ -0,11 +1,13 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
#version 300 es | ||
uniform sampler2D inTexture; | ||
uniform float textureSize; | ||
out vec4 outTexture; | ||
void main() | ||
{ | ||
int yIndex = gl_VertexID / int(textureSize); | ||
int xIndex = gl_VertexID - (yIndex * int(textureSize)); | ||
vec2 uv = (0.5 + vec2(float(xIndex), float(yIndex))) / textureSize; | ||
int yIndex = gl_VertexID / int(maxWeight.textureSize); | ||
int xIndex = gl_VertexID - (yIndex * int(maxWeight.textureSize)); | ||
vec2 uv = (0.5 + vec2(float(xIndex), float(yIndex))) / maxWeight.textureSize; | ||
outTexture = texture(inTexture, uv); | ||
@@ -12,0 +14,0 @@ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); |
@@ -1,3 +0,3 @@ | ||
declare const _default: "#version 300 es\n#define SHADER_NAME triangle-layer-fragment-shader\n\nprecision highp float;\n\nuniform float opacity;\nuniform sampler2D weightsTexture;\nuniform sampler2D colorTexture;\nuniform float aggregationMode;\n\nin vec2 vTexCoords;\nin float vIntensityMin;\nin float vIntensityMax;\n\nout vec4 fragColor;\n\nvec4 getLinearColor(float value) {\n float factor = clamp(value * vIntensityMax, 0., 1.);\n vec4 color = texture(colorTexture, vec2(factor, 0.5));\n color.a *= min(value * vIntensityMin, 1.0);\n return color;\n}\n\nvoid main(void) {\n vec4 weights = texture(weightsTexture, vTexCoords);\n float weight = weights.r;\n\n if (aggregationMode > 0.5) {\n weight /= max(1.0, weights.a);\n }\n\n // discard pixels with 0 weight.\n if (weight <= 0.) {\n discard;\n }\n\n vec4 linearColor = getLinearColor(weight);\n linearColor.a *= opacity;\n fragColor = linearColor;\n}\n"; | ||
declare const _default: "#version 300 es\n#define SHADER_NAME triangle-layer-fragment-shader\n\nprecision highp float;\n\nuniform sampler2D weightsTexture;\nuniform sampler2D colorTexture;\n\nin vec2 vTexCoords;\nin float vIntensityMin;\nin float vIntensityMax;\n\nout vec4 fragColor;\n\nvec4 getLinearColor(float value) {\n float factor = clamp(value * vIntensityMax, 0., 1.);\n vec4 color = texture(colorTexture, vec2(factor, 0.5));\n color.a *= min(value * vIntensityMin, 1.0);\n return color;\n}\n\nvoid main(void) {\n vec4 weights = texture(weightsTexture, vTexCoords);\n float weight = weights.r;\n\n if (triangle.aggregationMode > 0.5) {\n weight /= max(1.0, weights.a);\n }\n\n // discard pixels with 0 weight.\n if (weight <= 0.) {\n discard;\n }\n\n vec4 linearColor = getLinearColor(weight);\n linearColor.a *= layer.opacity;\n fragColor = linearColor;\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=triangle-layer-fragment.glsl.d.ts.map |
@@ -1,20 +0,4 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -24,6 +8,4 @@ #version 300 es | ||
precision highp float; | ||
uniform float opacity; | ||
uniform sampler2D weightsTexture; | ||
uniform sampler2D colorTexture; | ||
uniform float aggregationMode; | ||
in vec2 vTexCoords; | ||
@@ -42,3 +24,3 @@ in float vIntensityMin; | ||
float weight = weights.r; | ||
if (aggregationMode > 0.5) { | ||
if (triangle.aggregationMode > 0.5) { | ||
weight /= max(1.0, weights.a); | ||
@@ -50,5 +32,5 @@ } | ||
vec4 linearColor = getLinearColor(weight); | ||
linearColor.a *= opacity; | ||
linearColor.a *= layer.opacity; | ||
fragColor = linearColor; | ||
} | ||
`; |
@@ -1,3 +0,3 @@ | ||
declare const _default: "#version 300 es\n#define SHADER_NAME heatp-map-layer-vertex-shader\n\nuniform sampler2D maxTexture;\nuniform float intensity;\nuniform vec2 colorDomain;\nuniform float threshold;\nuniform float aggregationMode;\n\nin vec3 positions;\nin vec2 texCoords;\n\nout vec2 vTexCoords;\nout float vIntensityMin;\nout float vIntensityMax;\n\nvoid main(void) {\n gl_Position = project_position_to_clipspace(positions, vec3(0.0), vec3(0.0));\n vTexCoords = texCoords;\n vec4 maxTexture = texture(maxTexture, vec2(0.5));\n float maxValue = aggregationMode < 0.5 ? maxTexture.r : maxTexture.g;\n float minValue = maxValue * threshold;\n if (colorDomain[1] > 0.) {\n // if user specified custom domain use it.\n maxValue = colorDomain[1];\n minValue = colorDomain[0];\n }\n vIntensityMax = intensity / maxValue;\n vIntensityMin = intensity / minValue;\n}\n"; | ||
declare const _default: "#version 300 es\n#define SHADER_NAME heatp-map-layer-vertex-shader\n\nuniform sampler2D maxTexture;\n\nin vec3 positions;\nin vec2 texCoords;\n\nout vec2 vTexCoords;\nout float vIntensityMin;\nout float vIntensityMax;\n\nvoid main(void) {\n gl_Position = project_position_to_clipspace(positions, vec3(0.0), vec3(0.0));\n vTexCoords = texCoords;\n vec4 maxTexture = texture(maxTexture, vec2(0.5));\n float maxValue = triangle.aggregationMode < 0.5 ? maxTexture.r : maxTexture.g;\n float minValue = maxValue * triangle.threshold;\n if (triangle.colorDomain[1] > 0.) {\n // if user specified custom domain use it.\n maxValue = triangle.colorDomain[1];\n minValue = triangle.colorDomain[0];\n }\n vIntensityMax = triangle.intensity / maxValue;\n vIntensityMin = triangle.intensity / minValue;\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=triangle-layer-vertex.glsl.d.ts.map |
@@ -1,20 +0,4 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
// Inspired by screen-grid-layer vertex shader in deck.gl | ||
@@ -25,6 +9,2 @@ export default `\ | ||
uniform sampler2D maxTexture; | ||
uniform float intensity; | ||
uniform vec2 colorDomain; | ||
uniform float threshold; | ||
uniform float aggregationMode; | ||
in vec3 positions; | ||
@@ -39,11 +19,11 @@ in vec2 texCoords; | ||
vec4 maxTexture = texture(maxTexture, vec2(0.5)); | ||
float maxValue = aggregationMode < 0.5 ? maxTexture.r : maxTexture.g; | ||
float minValue = maxValue * threshold; | ||
if (colorDomain[1] > 0.) { | ||
maxValue = colorDomain[1]; | ||
minValue = colorDomain[0]; | ||
float maxValue = triangle.aggregationMode < 0.5 ? maxTexture.r : maxTexture.g; | ||
float minValue = maxValue * triangle.threshold; | ||
if (triangle.colorDomain[1] > 0.) { | ||
maxValue = triangle.colorDomain[1]; | ||
minValue = triangle.colorDomain[0]; | ||
} | ||
vIntensityMax = intensity / maxValue; | ||
vIntensityMin = intensity / minValue; | ||
vIntensityMax = triangle.intensity / maxValue; | ||
vIntensityMin = triangle.intensity / minValue; | ||
} | ||
`; |
@@ -11,4 +11,4 @@ import type { Buffer, Device, Texture } from '@luma.gl/core'; | ||
}; | ||
colorDomain: number[]; | ||
aggregationMode: string; | ||
colorDomain: [number, number]; | ||
aggregationMode: number; | ||
threshold: number; | ||
@@ -28,14 +28,8 @@ intensity: number; | ||
}; | ||
getShaders(): { | ||
vs: string; | ||
fs: string; | ||
modules: import("@luma.gl/shadertools").ShaderModule[]; | ||
}; | ||
getShaders(): any; | ||
initializeState({ device }: LayerContext): void; | ||
_getModel(device: Device): Model; | ||
draw({ uniforms }: { | ||
uniforms: any; | ||
}): void; | ||
draw(): void; | ||
} | ||
export {}; | ||
//# sourceMappingURL=triangle-layer.d.ts.map |
@@ -1,20 +0,4 @@ | ||
// Copyright (c) 2015 - 2019 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { Model } from '@luma.gl/engine'; | ||
@@ -24,5 +8,6 @@ import { Layer, project32 } from '@deck.gl/core'; | ||
import fs from "./triangle-layer-fragment.glsl.js"; | ||
import { triangleUniforms } from "./triangle-layer-uniforms.js"; | ||
class TriangleLayer extends Layer { | ||
getShaders() { | ||
return { vs, fs, modules: [project32] }; | ||
return super.getShaders({ vs, fs, modules: [project32, triangleUniforms] }); | ||
} | ||
@@ -33,7 +18,6 @@ initializeState({ device }) { | ||
_getModel(device) { | ||
const { vertexCount, data, weightsTexture, maxTexture, colorTexture } = this.props; | ||
const { vertexCount, data } = this.props; | ||
return new Model(device, { | ||
...this.getShaders(), | ||
id: this.props.id, | ||
bindings: { weightsTexture, maxTexture, colorTexture }, | ||
attributes: data.attributes, | ||
@@ -44,16 +28,19 @@ bufferLayout: [ | ||
], | ||
topology: 'triangle-fan-webgl', | ||
topology: 'triangle-strip', | ||
vertexCount | ||
}); | ||
} | ||
draw({ uniforms }) { | ||
draw() { | ||
const { model } = this.state; | ||
const { intensity, threshold, aggregationMode, colorDomain } = this.props; | ||
model.setUniforms({ | ||
...uniforms, | ||
const { aggregationMode, colorDomain, intensity, threshold, colorTexture, maxTexture, weightsTexture } = this.props; | ||
const triangleProps = { | ||
aggregationMode, | ||
colorDomain, | ||
intensity, | ||
threshold, | ||
aggregationMode, | ||
colorDomain | ||
}); | ||
colorTexture, | ||
maxTexture, | ||
weightsTexture | ||
}; | ||
model.shaderInputs.setProps({ triangle: triangleProps }); | ||
model.draw(this.context.renderPass); | ||
@@ -60,0 +47,0 @@ } |
@@ -0,1 +1,4 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -2,0 +5,0 @@ #version 300 es |
@@ -1,3 +0,3 @@ | ||
declare const _default: "#version 300 es\nin vec3 positions;\nin vec3 positions64Low;\nin float weights;\nout vec4 weightsTexture;\nuniform float radiusPixels;\nuniform float textureWidth;\nuniform vec4 commonBounds;\nuniform float weightsScale;\n\nvoid main()\n{\n weightsTexture = vec4(weights * weightsScale, 0., 0., 1.);\n\n float radiusTexels = project_pixel_size(radiusPixels) * textureWidth / (commonBounds.z - commonBounds.x);\n gl_PointSize = radiusTexels * 2.;\n\n vec3 commonPosition = project_position(positions, positions64Low);\n\n // // map xy from commonBounds to [-1, 1]\n gl_Position.xy = (commonPosition.xy - commonBounds.xy) / (commonBounds.zw - commonBounds.xy) ;\n gl_Position.xy = (gl_Position.xy * 2.) - (1.);\n gl_Position.w = 1.0;\n}\n"; | ||
declare const _default: "#version 300 es\nin vec3 positions;\nin vec3 positions64Low;\nin float weights;\nout vec4 weightsTexture;\n\nvoid main()\n{\n weightsTexture = vec4(weights * weight.weightsScale, 0., 0., 1.);\n\n float radiusTexels = project_pixel_size(weight.radiusPixels) * weight.textureWidth / (weight.commonBounds.z - weight.commonBounds.x);\n gl_PointSize = radiusTexels * 2.;\n\n vec3 commonPosition = project_position(positions, positions64Low);\n\n // // map xy from commonBounds to [-1, 1]\n gl_Position.xy = (commonPosition.xy - weight.commonBounds.xy) / (weight.commonBounds.zw - weight.commonBounds.xy) ;\n gl_Position.xy = (gl_Position.xy * 2.) - (1.);\n gl_Position.w = 1.0;\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=weights-vs.glsl.d.ts.map |
@@ -0,1 +1,4 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -7,13 +10,9 @@ #version 300 es | ||
out vec4 weightsTexture; | ||
uniform float radiusPixels; | ||
uniform float textureWidth; | ||
uniform vec4 commonBounds; | ||
uniform float weightsScale; | ||
void main() | ||
{ | ||
weightsTexture = vec4(weights * weightsScale, 0., 0., 1.); | ||
float radiusTexels = project_pixel_size(radiusPixels) * textureWidth / (commonBounds.z - commonBounds.x); | ||
weightsTexture = vec4(weights * weight.weightsScale, 0., 0., 1.); | ||
float radiusTexels = project_pixel_size(weight.radiusPixels) * weight.textureWidth / (weight.commonBounds.z - weight.commonBounds.x); | ||
gl_PointSize = radiusTexels * 2.; | ||
vec3 commonPosition = project_position(positions, positions64Low); | ||
gl_Position.xy = (commonPosition.xy - commonBounds.xy) / (commonBounds.zw - commonBounds.xy) ; | ||
gl_Position.xy = (commonPosition.xy - weight.commonBounds.xy) / (weight.commonBounds.zw - weight.commonBounds.xy) ; | ||
gl_Position.xy = (gl_Position.xy * 2.) - (1.); | ||
@@ -20,0 +19,0 @@ gl_Position.w = 1.0; |
@@ -1,10 +0,11 @@ | ||
import { Accessor, AccessorFunction, Color, Position, Material, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import { ColumnLayer } from '@deck.gl/layers'; | ||
import CPUAggregator from "../utils/cpu-aggregator.js"; | ||
import AggregationLayer, { AggregationLayerProps } from "../aggregation-layer.js"; | ||
import { AggregateAccessor } from "../types.js"; | ||
/** All properties supported by by HexagonLayer. */ | ||
export type HexagonLayerProps<DataT = unknown> = _HexagonLayerProps<DataT> & AggregationLayerProps<DataT>; | ||
import { Accessor, Color, GetPickingInfoParams, CompositeLayerProps, Layer, Material, LayersList, PickingInfo, Position, Viewport, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator, AggregationOperation } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import { AggregateAccessor } from "../common/types.js"; | ||
import { AttributeWithScale } from "../common/utils/scale-utils.js"; | ||
import { BinOptions } from "./bin-options-uniforms.js"; | ||
/** All properties supported by HexagonLayer. */ | ||
export type HexagonLayerProps<DataT = unknown> = _HexagonLayerProps<DataT> & CompositeLayerProps; | ||
/** Properties added by HexagonLayer. */ | ||
type _HexagonLayerProps<DataT = unknown> = { | ||
type _HexagonLayerProps<DataT> = { | ||
/** | ||
@@ -16,8 +17,9 @@ * Radius of hexagon bin in meters. The hexagons are pointy-topped (rather than flat-topped). | ||
/** | ||
* Function to aggregate data into hexagonal bins. | ||
* @default d3-hexbin | ||
* Custom accessor to retrieve a hexagonal bin index from each data object. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
*/ | ||
hexagonAggregator?: (props: any, params: any) => any; | ||
hexagonAggregator?: ((position: number[], radius: number) => [number, number]) | null; | ||
/** | ||
* Color scale input domain. | ||
* Color scale domain, default is set to the extent of aggregated weights in each cell. | ||
* @default [min(colorWeight), max(colorWeight)] | ||
@@ -27,8 +29,7 @@ */ | ||
/** | ||
* Specified as an array of colors [color1, color2, ...]. | ||
* @default `6-class YlOrRd` - [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) | ||
* Default: [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) `6-class YlOrRd` | ||
*/ | ||
colorRange?: Color[]; | ||
/** | ||
* Hexagon radius multiplier, clamped between 0 - 1. | ||
* Cell size multiplier, clamped between 0 - 1. | ||
* @default 1 | ||
@@ -38,3 +39,3 @@ */ | ||
/** | ||
* Elevation scale input domain. The elevation scale is a linear scale that maps number of counts to elevation. | ||
* Elevation scale input domain, default is set to between 0 and the max of aggregated weights in each cell. | ||
* @default [0, max(elevationWeight)] | ||
@@ -49,3 +50,3 @@ */ | ||
/** | ||
* Hexagon elevation multiplier. | ||
* Cell elevation multiplier. | ||
* @default 1 | ||
@@ -56,8 +57,8 @@ */ | ||
* Whether to enable cell elevation. If set to false, all cell will be flat. | ||
* @default false | ||
* @default true | ||
*/ | ||
extruded?: boolean; | ||
/** | ||
* Filter bins and re-calculate color by `upperPercentile`. | ||
* Hexagons with color value larger than the `upperPercentile` will be hidden. | ||
* Filter cells and re-calculate color by `upperPercentile`. | ||
* Cells with value larger than the upperPercentile will be hidden. | ||
* @default 100 | ||
@@ -67,4 +68,4 @@ */ | ||
/** | ||
* Filter bins and re-calculate color by `lowerPercentile`. | ||
* Hexagons with color value smaller than the `lowerPercentile` will be hidden. | ||
* Filter cells and re-calculate color by `lowerPercentile`. | ||
* Cells with value smaller than the lowerPercentile will be hidden. | ||
* @default 0 | ||
@@ -74,4 +75,4 @@ */ | ||
/** | ||
* Filter bins and re-calculate elevation by `elevationUpperPercentile`. | ||
* Hexagons with elevation value larger than the `elevationUpperPercentile` will be hidden. | ||
* Filter cells and re-calculate elevation by `elevationUpperPercentile`. | ||
* Cells with elevation value larger than the `elevationUpperPercentile` will be hidden. | ||
* @default 100 | ||
@@ -81,4 +82,4 @@ */ | ||
/** | ||
* Filter bins and re-calculate elevation by `elevationLowerPercentile`. | ||
* Hexagons with elevation value larger than the `elevationLowerPercentile` will be hidden. | ||
* Filter cells and re-calculate elevation by `elevationLowerPercentile`. | ||
* Cells with elevation value larger than the `elevationLowerPercentile` will be hidden. | ||
* @default 0 | ||
@@ -89,8 +90,10 @@ */ | ||
* Scaling function used to determine the color of the grid cell, default value is 'quantize'. | ||
* Supported Values are 'quantize', 'quantile' and 'ordinal'. | ||
* Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'. | ||
* @default 'quantize' | ||
*/ | ||
colorScaleType?: 'quantize' | 'quantile' | 'ordinal'; | ||
colorScaleType?: 'quantize' | 'linear' | 'quantile' | 'ordinal'; | ||
/** | ||
* Scaling function used to determine the elevation of the grid cell, only supports 'linear'. | ||
* Supported Values are 'linear' and 'quantile'. | ||
* @default 'linear' | ||
*/ | ||
@@ -107,10 +110,14 @@ elevationScaleType?: 'linear'; | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's color value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
colorAggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
colorAggregation?: AggregationOperation; | ||
/** | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's elevation value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
elevationAggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
elevationAggregation?: AggregationOperation; | ||
/** | ||
@@ -120,5 +127,5 @@ * Method called to retrieve the position of each object. | ||
*/ | ||
getPosition?: AccessorFunction<DataT, Position>; | ||
getPosition?: Accessor<DataT, Position>; | ||
/** | ||
* The weight of a data object used to calculate the color value for a bin. | ||
* The weight of a data object used to calculate the color value for a cell. | ||
* @default 1 | ||
@@ -128,3 +135,4 @@ */ | ||
/** | ||
* After data objects are aggregated into bins, this accessor is called on each cell to get the value that its color is based on. | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its color is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
@@ -134,3 +142,3 @@ */ | ||
/** | ||
* The weight of a data object used to calculate the elevation value for a bin. | ||
* The weight of a data object used to calculate the elevation value for a cell. | ||
* @default 1 | ||
@@ -140,3 +148,4 @@ */ | ||
/** | ||
* After data objects are aggregated into bins, this accessor is called on each cell to get the value that its elevation is based on. | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its elevation is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
@@ -146,3 +155,3 @@ */ | ||
/** | ||
* This callback will be called when cell color domain has been calculated. | ||
* This callback will be called when bin color domain has been calculated. | ||
* @default () => {} | ||
@@ -152,3 +161,3 @@ */ | ||
/** | ||
* This callback will be called when cell elevation domain has been calculated. | ||
* This callback will be called when bin elevation domain has been calculated. | ||
* @default () => {} | ||
@@ -158,27 +167,48 @@ */ | ||
/** | ||
* (Experimental) Filter data objects | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
* @default false | ||
*/ | ||
_filterData: null | ((d: DataT) => boolean); | ||
gpuAggregation?: boolean; | ||
}; | ||
/** Aggregates data into a hexagon-based heatmap. The color and height of a hexagon are determined based on the objects it contains. */ | ||
export default class HexagonLayer<DataT, ExtraPropsT extends {} = {}> extends AggregationLayer<DataT, ExtraPropsT & Required<_HexagonLayerProps<DataT>>> { | ||
export type HexagonLayerPickingInfo<DataT> = PickingInfo<{ | ||
/** Column index of the picked cell */ | ||
col: number; | ||
/** Row index of the picked cell */ | ||
row: number; | ||
/** Aggregated color value, as determined by `getColorWeight` and `colorAggregation` */ | ||
colorValue: number; | ||
/** Aggregated elevation value, as determined by `getElevationWeight` and `elevationAggregation` */ | ||
elevationValue: number; | ||
/** Number of data points in the picked cell */ | ||
count: number; | ||
/** Centroid of the hexagon */ | ||
position: [number, number]; | ||
/** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */ | ||
pointIndices?: number[]; | ||
/** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */ | ||
points?: DataT[]; | ||
}>; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
export default class HexagonLayer<DataT = any, ExtraPropsT extends {} = {}> extends AggregationLayer<DataT, ExtraPropsT & Required<_HexagonLayerProps<DataT>>> { | ||
static layerName: string; | ||
static defaultProps: DefaultProps<HexagonLayerProps<unknown>>; | ||
state: AggregationLayer<DataT>['state'] & { | ||
cpuAggregator: CPUAggregator; | ||
aggregatorState: CPUAggregator['state']; | ||
vertices: number[][] | null; | ||
state: AggregationLayer<DataT>['state'] & BinOptions & { | ||
dataAsArray?: DataT[]; | ||
colors?: AttributeWithScale; | ||
elevations?: AttributeWithScale; | ||
binIdRange: [number, number][]; | ||
aggregatorViewport: Viewport; | ||
}; | ||
getAggregatorType(): string; | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator; | ||
initializeState(): void; | ||
updateState(opts: UpdateParameters<this>): void; | ||
convertLatLngToMeterOffset(hexagonVertices: any): number[][] | null; | ||
getPickingInfo({ info }: { | ||
info: any; | ||
}): any; | ||
_onGetSublayerColor(cell: any): any; | ||
_onGetSublayerElevation(cell: any): any; | ||
_getSublayerUpdateTriggers(): {}; | ||
renderLayers(): ColumnLayer<any, {}>; | ||
updateState(params: UpdateParameters<this>): boolean; | ||
private _updateBinOptions; | ||
draw(opts: any): void; | ||
private _onAggregationUpdate; | ||
onAttributeChange(id: string): void; | ||
renderLayers(): LayersList | Layer | null; | ||
getPickingInfo(params: GetPickingInfoParams): HexagonLayerPickingInfo<DataT>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=hexagon-layer.d.ts.map |
@@ -1,171 +0,355 @@ | ||
// 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 { log } from '@deck.gl/core'; | ||
import { ColumnLayer } from '@deck.gl/layers'; | ||
import { defaultColorRange } from "../utils/color-utils.js"; | ||
import { pointToHexbin } from "./hexagon-aggregator.js"; | ||
import CPUAggregator from "../utils/cpu-aggregator.js"; | ||
import AggregationLayer from "../aggregation-layer.js"; | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { log, createIterable, project32, Viewport } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import { defaultColorRange } from "../common/utils/color-utils.js"; | ||
import { AttributeWithScale } from "../common/utils/scale-utils.js"; | ||
import { getBinIdRange } from "../common/utils/bounds-utils.js"; | ||
import HexagonCellLayer from "./hexagon-cell-layer.js"; | ||
import { pointToHexbin, HexbinVertices, getHexbinCentroid, pointToHexbinGLSL } from "./hexbin.js"; | ||
import { binOptionsUniforms } from "./bin-options-uniforms.js"; | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
function nop() { } | ||
function noop() { } | ||
const defaultProps = { | ||
gpuAggregation: false, | ||
// color | ||
colorDomain: null, | ||
colorRange: defaultColorRange, | ||
getColorValue: { type: 'accessor', value: null }, // default value is calcuated from `getColorWeight` and `colorAggregation` | ||
getColorValue: { type: 'accessor', value: null }, // default value is calculated from `getColorWeight` and `colorAggregation` | ||
getColorWeight: { type: 'accessor', value: 1 }, | ||
colorAggregation: 'SUM', | ||
lowerPercentile: { type: 'number', value: 0, min: 0, max: 100 }, | ||
upperPercentile: { type: 'number', value: 100, min: 0, max: 100 }, | ||
lowerPercentile: { type: 'number', min: 0, max: 100, value: 0 }, | ||
upperPercentile: { type: 'number', min: 0, max: 100, value: 100 }, | ||
colorScaleType: 'quantize', | ||
onSetColorDomain: nop, | ||
onSetColorDomain: noop, | ||
// elevation | ||
elevationDomain: null, | ||
elevationRange: [0, 1000], | ||
getElevationValue: { type: 'accessor', value: null }, // default value is calcuated from `getElevationWeight` and `elevationAggregation` | ||
getElevationValue: { type: 'accessor', value: null }, // default value is calculated from `getElevationWeight` and `elevationAggregation` | ||
getElevationWeight: { type: 'accessor', value: 1 }, | ||
elevationAggregation: 'SUM', | ||
elevationLowerPercentile: { type: 'number', value: 0, min: 0, max: 100 }, | ||
elevationUpperPercentile: { type: 'number', value: 100, min: 0, max: 100 }, | ||
elevationScale: { type: 'number', min: 0, value: 1 }, | ||
elevationLowerPercentile: { type: 'number', min: 0, max: 100, value: 0 }, | ||
elevationUpperPercentile: { type: 'number', min: 0, max: 100, value: 100 }, | ||
elevationScaleType: 'linear', | ||
onSetElevationDomain: nop, | ||
radius: { type: 'number', value: 1000, min: 1 }, | ||
onSetElevationDomain: noop, | ||
// hexbin | ||
radius: { type: 'number', min: 1, value: 1000 }, | ||
coverage: { type: 'number', min: 0, max: 1, value: 1 }, | ||
getPosition: { type: 'accessor', value: (x) => x.position }, | ||
hexagonAggregator: { type: 'function', optional: true, value: null }, | ||
extruded: false, | ||
hexagonAggregator: pointToHexbin, | ||
getPosition: { type: 'accessor', value: (x) => x.position }, | ||
// Optional material for 'lighting' shader module | ||
material: true, | ||
// data filter | ||
_filterData: { type: 'function', value: null, optional: true } | ||
material: true | ||
}; | ||
/** Aggregates data into a hexagon-based heatmap. The color and height of a hexagon are determined based on the objects it contains. */ | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
class HexagonLayer extends AggregationLayer { | ||
getAggregatorType() { | ||
const { gpuAggregation, hexagonAggregator, getColorValue, getElevationValue } = this.props; | ||
if (gpuAggregation && (hexagonAggregator || getColorValue || getElevationValue)) { | ||
// If these features are desired by the app, the user should explicitly use CPU aggregation | ||
log.warn('Features not supported by GPU aggregation, falling back to CPU')(); | ||
return 'cpu'; | ||
} | ||
if ( | ||
// GPU aggregation is requested | ||
gpuAggregation && | ||
// GPU aggregation is supported by the device | ||
WebGLAggregator.isSupported(this.context.device)) { | ||
return 'gpu'; | ||
} | ||
return 'cpu'; | ||
} | ||
createAggregator(type) { | ||
if (type === 'cpu') { | ||
const { hexagonAggregator, radius } = this.props; | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({ positions }, index, opts) => { | ||
if (hexagonAggregator) { | ||
return hexagonAggregator(positions, radius); | ||
} | ||
const viewport = this.state.aggregatorViewport; | ||
// project to common space | ||
const p = viewport.projectPosition(positions); | ||
const { radiusCommon, hexOriginCommon } = opts; | ||
return pointToHexbin([p[0] - hexOriginCommon[0], p[1] - hexOriginCommon[1]], radiusCommon); | ||
} | ||
}, | ||
getValue: [ | ||
{ sources: ['colorWeights'], getValue: ({ colorWeights }) => colorWeights }, | ||
{ sources: ['elevationWeights'], getValue: ({ elevationWeights }) => elevationWeights } | ||
] | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 2, | ||
bufferLayout: this.getAttributeManager().getBufferLayouts({ isInstanced: false }), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: /* glsl */ ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float colorWeights; | ||
in float elevationWeights; | ||
${pointToHexbinGLSL} | ||
void getBin(out ivec2 binId) { | ||
vec3 positionCommon = project_position(positions, positions64Low); | ||
binId = pointToHexbin(positionCommon.xy, binOptions.radiusCommon); | ||
} | ||
void getValue(out vec2 value) { | ||
value = vec2(colorWeights, elevationWeights); | ||
} | ||
` | ||
}) | ||
}); | ||
} | ||
initializeState() { | ||
const cpuAggregator = new CPUAggregator({ | ||
getAggregator: props => props.hexagonAggregator, | ||
getCellSize: props => props.radius | ||
}); | ||
this.state = { | ||
cpuAggregator, | ||
aggregatorState: cpuAggregator.state, | ||
vertices: null | ||
}; | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager(); | ||
attributeManager.add({ | ||
positions: { size: 3, type: 'float64', accessor: 'getPosition' } | ||
positions: { | ||
size: 3, | ||
accessor: 'getPosition', | ||
type: 'float64', | ||
fp64: this.use64bitPositions() | ||
}, | ||
colorWeights: { size: 1, accessor: 'getColorWeight' }, | ||
elevationWeights: { size: 1, accessor: 'getElevationWeight' } | ||
}); | ||
// color and elevation attributes can't be added as attributes | ||
// they are calculated using 'getValue' accessor that takes an array of pints. | ||
} | ||
updateState(opts) { | ||
super.updateState(opts); | ||
if (opts.changeFlags.propsOrDataChanged) { | ||
const aggregatorState = this.state.cpuAggregator.updateState(opts, { | ||
viewport: this.context.viewport, | ||
attributes: this.getAttributes() | ||
updateState(params) { | ||
const aggregatorChanged = super.updateState(params); | ||
const { props, oldProps, changeFlags } = params; | ||
const { aggregator } = this.state; | ||
if ((changeFlags.dataChanged || !this.state.dataAsArray) && | ||
(props.getColorValue || props.getElevationValue)) { | ||
// Convert data to array | ||
this.state.dataAsArray = Array.from(createIterable(props.data).iterable); | ||
} | ||
if (aggregatorChanged || | ||
changeFlags.dataChanged || | ||
props.radius !== oldProps.radius || | ||
props.getColorValue !== oldProps.getColorValue || | ||
props.getElevationValue !== oldProps.getElevationValue || | ||
props.colorAggregation !== oldProps.colorAggregation || | ||
props.elevationAggregation !== oldProps.elevationAggregation) { | ||
this._updateBinOptions(); | ||
const { radiusCommon, hexOriginCommon, binIdRange, dataAsArray } = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
pointCount: this.getNumInstances(), | ||
operations: [props.colorAggregation, props.elevationAggregation], | ||
binOptions: { | ||
radiusCommon, | ||
hexOriginCommon | ||
}, | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
if (this.state.aggregatorState.layerData !== aggregatorState.layerData) { | ||
// if user provided custom aggregator and returns hexagonVertices, | ||
// Need to recalculate radius and angle based on vertices | ||
// @ts-expect-error | ||
const { hexagonVertices } = aggregatorState.layerData || {}; | ||
this.setState({ | ||
vertices: hexagonVertices && this.convertLatLngToMeterOffset(hexagonVertices) | ||
if (dataAsArray) { | ||
const { getColorValue, getElevationValue } = this.props; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by CPUAggregator | ||
customOperations: [ | ||
getColorValue && | ||
((indices) => getColorValue(indices.map(i => dataAsArray[i]), { indices, data: props.data })), | ||
getElevationValue && | ||
((indices) => getElevationValue(indices.map(i => dataAsArray[i]), { indices, data: props.data })) | ||
] | ||
}); | ||
} | ||
this.setState({ | ||
// make a copy of the internal state of cpuAggregator for testing | ||
aggregatorState | ||
}); | ||
} | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getColorValue) { | ||
aggregator.setNeedsUpdate(0); | ||
} | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getElevationValue) { | ||
aggregator.setNeedsUpdate(1); | ||
} | ||
return aggregatorChanged; | ||
} | ||
convertLatLngToMeterOffset(hexagonVertices) { | ||
const { viewport } = this.context; | ||
if (Array.isArray(hexagonVertices) && hexagonVertices.length === 6) { | ||
// get centroid of hexagons | ||
const vertex0 = hexagonVertices[0]; | ||
const vertex3 = hexagonVertices[3]; | ||
const centroid = [(vertex0[0] + vertex3[0]) / 2, (vertex0[1] + vertex3[1]) / 2]; | ||
const centroidFlat = viewport.projectFlat(centroid); | ||
const { metersPerUnit } = viewport.getDistanceScales(centroid); | ||
// offset all points by centroid to meter offset | ||
const vertices = hexagonVertices.map(vt => { | ||
const vtFlat = viewport.projectFlat(vt); | ||
return [ | ||
(vtFlat[0] - centroidFlat[0]) * metersPerUnit[0], | ||
(vtFlat[1] - centroidFlat[1]) * metersPerUnit[1] | ||
]; | ||
_updateBinOptions() { | ||
const bounds = this.getBounds(); | ||
let radiusCommon = 1; | ||
let hexOriginCommon = [0, 0]; | ||
let binIdRange = [ | ||
[0, 1], | ||
[0, 1] | ||
]; | ||
let viewport = this.context.viewport; | ||
if (bounds && Number.isFinite(bounds[0][0])) { | ||
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]; | ||
const { radius } = this.props; | ||
const { unitsPerMeter } = viewport.getDistanceScales(centroid); | ||
radiusCommon = unitsPerMeter[0] * radius; | ||
// Use the centroid of the hex at the center of the data | ||
// This offsets the common space without changing the bins | ||
const centerHex = pointToHexbin(viewport.projectFlat(centroid), radiusCommon); | ||
centroid = viewport.unprojectFlat(getHexbinCentroid(centerHex, radiusCommon)); | ||
const ViewportType = viewport.constructor; | ||
// We construct a viewport for the GPU aggregator's project module | ||
// This viewport is determined by data | ||
// removes arbitrary precision variance that depends on initial view state | ||
viewport = viewport.isGeospatial | ||
? new ViewportType({ longitude: centroid[0], latitude: centroid[1], zoom: 12 }) | ||
: new Viewport({ position: [centroid[0], centroid[1], 0], zoom: 12 }); | ||
hexOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])]; | ||
binIdRange = getBinIdRange({ | ||
dataBounds: bounds, | ||
getBinId: (p) => { | ||
const positionCommon = viewport.projectFlat(p); | ||
positionCommon[0] -= hexOriginCommon[0]; | ||
positionCommon[1] -= hexOriginCommon[1]; | ||
return pointToHexbin(positionCommon, radiusCommon); | ||
}, | ||
padding: 1 | ||
}); | ||
return vertices; | ||
} | ||
log.error('HexagonLayer: hexagonVertices needs to be an array of 6 points')(); | ||
return null; | ||
this.setState({ radiusCommon, hexOriginCommon, binIdRange, aggregatorViewport: viewport }); | ||
} | ||
getPickingInfo({ info }) { | ||
return this.state.cpuAggregator.getPickingInfo({ info }); | ||
draw(opts) { | ||
// Replaces render time viewport with our own | ||
if (opts.shaderModuleProps.project) { | ||
opts.shaderModuleProps.project.viewport = this.state.aggregatorViewport; | ||
} | ||
super.draw(opts); | ||
} | ||
// create a method for testing | ||
_onGetSublayerColor(cell) { | ||
return this.state.cpuAggregator.getAccessor('fillColor')(cell); | ||
_onAggregationUpdate({ channel }) { | ||
const props = this.getCurrentLayer().props; | ||
const { aggregator } = this.state; | ||
if (channel === 0) { | ||
const result = aggregator.getResult(0); | ||
this.setState({ | ||
colors: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetColorDomain(aggregator.getResultDomain(0)); | ||
} | ||
else if (channel === 1) { | ||
const result = aggregator.getResult(1); | ||
this.setState({ | ||
elevations: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetElevationDomain(aggregator.getResultDomain(1)); | ||
} | ||
} | ||
// create a method for testing | ||
_onGetSublayerElevation(cell) { | ||
return this.state.cpuAggregator.getAccessor('elevation')(cell); | ||
onAttributeChange(id) { | ||
const { aggregator } = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
this._updateBinOptions(); | ||
const { radiusCommon, hexOriginCommon, binIdRange } = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
binOptions: { | ||
radiusCommon, | ||
hexOriginCommon | ||
} | ||
}); | ||
break; | ||
case 'colorWeights': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
case 'elevationWeights': | ||
aggregator.setNeedsUpdate(1); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
} | ||
_getSublayerUpdateTriggers() { | ||
return this.state.cpuAggregator.getUpdateTriggers(this.props); | ||
} | ||
renderLayers() { | ||
const { elevationScale, extruded, coverage, material, transitions } = this.props; | ||
const { aggregatorState, vertices } = this.state; | ||
const SubLayerClass = this.getSubLayerClass('hexagon-cell', ColumnLayer); | ||
const updateTriggers = this._getSublayerUpdateTriggers(); | ||
const geometry = vertices | ||
? { vertices, radius: 1 } | ||
: { | ||
// default geometry | ||
// @ts-expect-error TODO - undefined property? | ||
radius: aggregatorState.layerData.radiusCommon || 1, | ||
radiusUnits: 'common', | ||
angle: 90 | ||
}; | ||
return new SubLayerClass({ | ||
...geometry, | ||
const { aggregator, radiusCommon, hexOriginCommon } = this.state; | ||
const { elevationScale, colorRange, elevationRange, extruded, coverage, material, transitions, colorScaleType, lowerPercentile, upperPercentile, colorDomain, elevationScaleType, elevationLowerPercentile, elevationUpperPercentile, elevationDomain } = this.props; | ||
const CellLayerClass = this.getSubLayerClass('cells', HexagonCellLayer); | ||
const binAttribute = aggregator.getBins(); | ||
const colors = this.state.colors?.update({ | ||
scaleType: colorScaleType, | ||
lowerPercentile, | ||
upperPercentile | ||
}); | ||
const elevations = this.state.elevations?.update({ | ||
scaleType: elevationScaleType, | ||
lowerPercentile: elevationLowerPercentile, | ||
upperPercentile: elevationUpperPercentile | ||
}); | ||
if (!colors || !elevations) { | ||
return null; | ||
} | ||
return new CellLayerClass(this.getSubLayerProps({ | ||
id: 'cells' | ||
}), { | ||
data: { | ||
length: aggregator.binCount, | ||
attributes: { | ||
getBin: binAttribute, | ||
getColorValue: colors.attribute, | ||
getElevationValue: elevations.attribute | ||
} | ||
}, | ||
// Data has changed shallowly, but we likely don't need to update the attributes | ||
dataComparator: (data, oldData) => data.length === oldData.length, | ||
updateTriggers: { | ||
getBin: [binAttribute], | ||
getColorValue: [colors.attribute], | ||
getElevationValue: [elevations.attribute] | ||
}, | ||
diskResolution: 6, | ||
vertices: HexbinVertices, | ||
radius: radiusCommon, | ||
hexOriginCommon, | ||
elevationScale, | ||
colorRange, | ||
colorScaleType, | ||
elevationRange, | ||
extruded, | ||
coverage, | ||
material, | ||
getFillColor: this._onGetSublayerColor.bind(this), | ||
getElevation: this._onGetSublayerElevation.bind(this), | ||
colorDomain: colors.domain || colorDomain || aggregator.getResultDomain(0), | ||
elevationDomain: elevations.domain || elevationDomain || aggregator.getResultDomain(1), | ||
colorCutoff: colors.cutoff, | ||
elevationCutoff: elevations.cutoff, | ||
transitions: transitions && { | ||
getFillColor: transitions.getColorValue || transitions.getColorWeight, | ||
getElevation: transitions.getElevationValue || transitions.getElevationWeight | ||
} | ||
}, this.getSubLayerProps({ | ||
id: 'hexagon-cell', | ||
updateTriggers | ||
}), { | ||
data: aggregatorState.layerData.data | ||
}, | ||
// Extensions are already handled by the GPUAggregator, do not pass it down | ||
extensions: [] | ||
}); | ||
} | ||
getPickingInfo(params) { | ||
const info = params.info; | ||
const { index } = info; | ||
if (index >= 0) { | ||
const bin = this.state.aggregator.getBin(index); | ||
let object; | ||
if (bin) { | ||
const centroidCommon = getHexbinCentroid(bin.id, this.state.radiusCommon); | ||
const centroid = this.context.viewport.unprojectFlat(centroidCommon); | ||
object = { | ||
col: bin.id[0], | ||
row: bin.id[1], | ||
position: centroid, | ||
colorValue: bin.value[0], | ||
elevationValue: bin.value[1], | ||
count: bin.count | ||
}; | ||
if (bin.pointIndices) { | ||
object.pointIndices = bin.pointIndices; | ||
object.points = Array.isArray(this.props.data) | ||
? bin.pointIndices.map(i => this.props.data[i]) | ||
: []; | ||
} | ||
} | ||
info.object = object; | ||
} | ||
return info; | ||
} | ||
} | ||
@@ -172,0 +356,0 @@ HexagonLayer.layerName = 'HexagonLayer'; |
export { default as ScreenGridLayer } from "./screen-grid-layer/screen-grid-layer.js"; | ||
export { default as CPUGridLayer } from "./cpu-grid-layer/cpu-grid-layer.js"; | ||
export { default as HexagonLayer } from "./hexagon-layer/hexagon-layer.js"; | ||
export { default as ContourLayer } from "./contour-layer/contour-layer.js"; | ||
export { default as GridLayer } from "./grid-layer/grid-layer.js"; | ||
export { default as GPUGridLayer } from "./gpu-grid-layer/gpu-grid-layer.js"; | ||
export { AGGREGATION_OPERATION } from "./utils/aggregation-operation-utils.js"; | ||
export { default as HeatmapLayer } from "./heatmap-layer/heatmap-layer.js"; | ||
export { default as _GPUGridAggregator } from "./utils/gpu-grid-aggregation/gpu-grid-aggregator.js"; | ||
export { default as _CPUAggregator } from "./utils/cpu-aggregator.js"; | ||
export { default as _AggregationLayer } from "./aggregation-layer.js"; | ||
export { default as _BinSorter } from "./utils/bin-sorter.js"; | ||
export type { ContourLayerProps } from "./contour-layer/contour-layer.js"; | ||
export { default as _AggregationLayer } from "./common/aggregation-layer.js"; | ||
export { WebGLAggregator, CPUAggregator } from "./common/aggregator/index.js"; | ||
export type { ContourLayerProps, ContourLayerPickingInfo } from "./contour-layer/contour-layer.js"; | ||
export type { HeatmapLayerProps } from "./heatmap-layer/heatmap-layer.js"; | ||
export type { HexagonLayerProps } from "./hexagon-layer/hexagon-layer.js"; | ||
export type { CPUGridLayerProps } from "./cpu-grid-layer/cpu-grid-layer.js"; | ||
export type { GridLayerProps } from "./grid-layer/grid-layer.js"; | ||
export type { GPUGridLayerProps } from "./gpu-grid-layer/gpu-grid-layer.js"; | ||
export type { ScreenGridLayerProps } from "./screen-grid-layer/screen-grid-layer.js"; | ||
export type { HexagonLayerProps, HexagonLayerPickingInfo } from "./hexagon-layer/hexagon-layer.js"; | ||
export type { GridLayerProps, GridLayerPickingInfo } from "./grid-layer/grid-layer.js"; | ||
export type { ScreenGridLayerProps, ScreenGridLayerPickingInfo } from "./screen-grid-layer/screen-grid-layer.js"; | ||
export type { Aggregator, AggregationOperation, AggregationProps, WebGLAggregatorProps, CPUAggregatorProps } from "./common/aggregator/index.js"; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,32 +0,10 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export { default as ScreenGridLayer } from "./screen-grid-layer/screen-grid-layer.js"; | ||
export { default as CPUGridLayer } from "./cpu-grid-layer/cpu-grid-layer.js"; | ||
export { default as HexagonLayer } from "./hexagon-layer/hexagon-layer.js"; | ||
export { default as ContourLayer } from "./contour-layer/contour-layer.js"; | ||
export { default as GridLayer } from "./grid-layer/grid-layer.js"; | ||
export { default as GPUGridLayer } from "./gpu-grid-layer/gpu-grid-layer.js"; | ||
export { AGGREGATION_OPERATION } from "./utils/aggregation-operation-utils.js"; | ||
// experimental export | ||
export { default as HeatmapLayer } from "./heatmap-layer/heatmap-layer.js"; | ||
export { default as _GPUGridAggregator } from "./utils/gpu-grid-aggregation/gpu-grid-aggregator.js"; | ||
export { default as _CPUAggregator } from "./utils/cpu-aggregator.js"; | ||
export { default as _AggregationLayer } from "./aggregation-layer.js"; | ||
export { default as _BinSorter } from "./utils/bin-sorter.js"; | ||
export { default as _AggregationLayer } from "./common/aggregation-layer.js"; | ||
export { WebGLAggregator, CPUAggregator } from "./common/aggregator/index.js"; |
import { Texture } from '@luma.gl/core'; | ||
import { Model } from '@luma.gl/engine'; | ||
import { Layer, LayerProps, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import type { _ScreenGridLayerProps } from "./screen-grid-layer.js"; | ||
import { Layer, UpdateParameters, Color } from '@deck.gl/core'; | ||
import { ShaderModule } from '@luma.gl/shadertools'; | ||
/** All properties supported by ScreenGridCellLayer. */ | ||
export type ScreenGridCellLayerProps<DataT = unknown> = _ScreenGridCellLayerProps<DataT> & LayerProps; | ||
import type { ScaleType } from "../common/types.js"; | ||
/** Proprties added by ScreenGridCellLayer. */ | ||
export type _ScreenGridCellLayerProps<DataT> = _ScreenGridLayerProps<DataT> & { | ||
maxTexture: Texture; | ||
export type _ScreenGridCellLayerProps = { | ||
cellSizePixels: number; | ||
cellMarginPixels: number; | ||
colorScaleType: ScaleType; | ||
colorDomain: () => [number, number]; | ||
colorRange?: Color[]; | ||
}; | ||
export default class ScreenGridCellLayer<DataT = any, ExtraPropsT extends {} = {}> extends Layer<ExtraPropsT & Required<_ScreenGridCellLayerProps<DataT>>> { | ||
export default class ScreenGridCellLayer<ExtraPropsT extends {} = {}> extends Layer<ExtraPropsT & Required<_ScreenGridCellLayerProps>> { | ||
static layerName: string; | ||
static defaultProps: DefaultProps<ScreenGridCellLayerProps<unknown>>; | ||
state: { | ||
model?: Model; | ||
colorTexture: Texture; | ||
}; | ||
@@ -24,16 +26,9 @@ getShaders(): { | ||
initializeState(): void; | ||
shouldUpdateState({ changeFlags }: { | ||
changeFlags: any; | ||
}): any; | ||
updateState(params: UpdateParameters<this>): void; | ||
finalizeState(context: any): void; | ||
draw({ uniforms }: { | ||
uniforms: any; | ||
}): void; | ||
calculateInstancePositions(attribute: any, { numInstances }: { | ||
numInstances: any; | ||
}): void; | ||
_getModel(): Model; | ||
_shouldUseMinMax(): boolean; | ||
_updateUniforms(oldProps: any, props: any, changeFlags: any): void; | ||
} | ||
//# sourceMappingURL=screen-grid-cell-layer.d.ts.map |
@@ -1,105 +0,66 @@ | ||
// Copyright (c) 2015 - 2019 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { Model, Geometry } from '@luma.gl/engine'; | ||
import { Layer, log, picking } from '@deck.gl/core'; | ||
import { defaultColorRange, colorRangeToFlatArray } from "../utils/color-utils.js"; | ||
import { Layer, picking } from '@deck.gl/core'; | ||
import { createColorRangeTexture, updateColorRangeTexture } from "../common/utils/color-utils.js"; | ||
import vs from "./screen-grid-layer-vertex.glsl.js"; | ||
import fs from "./screen-grid-layer-fragment.glsl.js"; | ||
const DEFAULT_MINCOLOR = [0, 0, 0, 0]; | ||
const DEFAULT_MAXCOLOR = [0, 255, 0, 255]; | ||
const COLOR_PROPS = ['minColor', 'maxColor', 'colorRange', 'colorDomain']; | ||
const defaultProps = { | ||
cellSizePixels: { type: 'number', value: 100, min: 1 }, | ||
cellMarginPixels: { type: 'number', value: 2, min: 0, max: 5 }, | ||
colorDomain: null, | ||
colorRange: defaultColorRange | ||
}; | ||
import { screenGridUniforms } from "./screen-grid-layer-uniforms.js"; | ||
class ScreenGridCellLayer extends Layer { | ||
getShaders() { | ||
return { vs, fs, modules: [picking] }; | ||
return super.getShaders({ vs, fs, modules: [picking, screenGridUniforms] }); | ||
} | ||
initializeState() { | ||
const attributeManager = this.getAttributeManager(); | ||
attributeManager.addInstanced({ | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
instancePositions: { size: 3, update: this.calculateInstancePositions }, | ||
instanceCounts: { size: 4, noAlloc: true } | ||
this.getAttributeManager().addInstanced({ | ||
instancePositions: { | ||
size: 2, | ||
type: 'float32', | ||
accessor: 'getBin' | ||
}, | ||
instanceWeights: { | ||
size: 1, | ||
type: 'float32', | ||
accessor: 'getWeight' | ||
} | ||
}); | ||
this.setState({ | ||
model: this._getModel() | ||
}); | ||
this.state.model = this._getModel(); | ||
} | ||
shouldUpdateState({ changeFlags }) { | ||
// 'instanceCounts' buffer contetns change on viewport change. | ||
return changeFlags.somethingChanged; | ||
} | ||
updateState(params) { | ||
super.updateState(params); | ||
const { oldProps, props, changeFlags } = params; | ||
const attributeManager = this.getAttributeManager(); | ||
if (props.numInstances !== oldProps.numInstances) { | ||
attributeManager.invalidateAll(); | ||
const { props, oldProps, changeFlags } = params; | ||
const model = this.state.model; | ||
if (oldProps.colorRange !== props.colorRange) { | ||
this.state.colorTexture?.destroy(); | ||
this.state.colorTexture = createColorRangeTexture(this.context.device, props.colorRange, props.colorScaleType); | ||
const screenGridProps = { colorRange: this.state.colorTexture }; | ||
model.shaderInputs.setProps({ screenGrid: screenGridProps }); | ||
} | ||
else if (oldProps.cellSizePixels !== props.cellSizePixels) { | ||
attributeManager.invalidate('instancePositions'); | ||
else if (oldProps.colorScaleType !== props.colorScaleType) { | ||
updateColorRangeTexture(this.state.colorTexture, props.colorScaleType); | ||
} | ||
this._updateUniforms(oldProps, props, changeFlags); | ||
if (oldProps.cellMarginPixels !== props.cellMarginPixels || | ||
oldProps.cellSizePixels !== props.cellSizePixels || | ||
changeFlags.viewportChanged) { | ||
const { width, height } = this.context.viewport; | ||
const { cellSizePixels: gridSize, cellMarginPixels } = this.props; | ||
const cellSize = Math.max(gridSize - cellMarginPixels, 0); | ||
const screenGridProps = { | ||
gridSizeClipspace: [(gridSize / width) * 2, (gridSize / height) * 2], | ||
cellSizeClipspace: [(cellSize / width) * 2, (cellSize / height) * 2] | ||
}; | ||
model.shaderInputs.setProps({ screenGrid: screenGridProps }); | ||
} | ||
} | ||
finalizeState(context) { | ||
super.finalizeState(context); | ||
this.state.colorTexture?.destroy(); | ||
} | ||
draw({ uniforms }) { | ||
const { parameters, maxTexture } = this.props; | ||
const minColor = this.props.minColor || DEFAULT_MINCOLOR; | ||
const maxColor = this.props.maxColor || DEFAULT_MAXCOLOR; | ||
// If colorDomain not specified we use default domain [1, maxCount] | ||
// maxCount value will be sampled form maxTexture in vertex shader. | ||
const colorDomain = this.props.colorDomain || [1, 0]; | ||
const colorDomain = this.props.colorDomain(); | ||
const model = this.state.model; | ||
model.setUniforms(uniforms); | ||
model.setBindings({ | ||
maxTexture | ||
}); | ||
model.setUniforms({ | ||
// @ts-expect-error stricter luma gl types | ||
minColor, | ||
// @ts-expect-error stricter luma gl types | ||
maxColor, | ||
colorDomain | ||
}); | ||
model.setParameters({ | ||
depthWriteEnabled: false, | ||
// How to specify depth mask in WebGPU? | ||
// depthMask: false, | ||
...parameters | ||
}); | ||
const screenGridProps = { colorDomain }; | ||
model.shaderInputs.setProps({ screenGrid: screenGridProps }); | ||
model.draw(this.context.renderPass); | ||
} | ||
calculateInstancePositions(attribute, { numInstances }) { | ||
const { width, height } = this.context.viewport; | ||
const { cellSizePixels } = this.props; | ||
const numCol = Math.ceil(width / cellSizePixels); | ||
const { value, size } = attribute; | ||
for (let i = 0; i < numInstances; i++) { | ||
const x = i % numCol; | ||
const y = Math.floor(i / numCol); | ||
value[i * size + 0] = ((x * cellSizePixels) / width) * 2 - 1; | ||
value[i * size + 1] = 1 - ((y * cellSizePixels) / height) * 2; | ||
value[i * size + 2] = 0; | ||
} | ||
} | ||
// Private Methods | ||
@@ -112,13 +73,8 @@ _getModel() { | ||
geometry: new Geometry({ | ||
topology: 'triangle-list', | ||
topology: 'triangle-strip', | ||
attributes: { | ||
// prettier-ignore | ||
positions: new Float32Array([ | ||
0, 0, 0, | ||
1, 0, 0, | ||
1, 1, 0, | ||
0, 0, 0, | ||
1, 1, 0, | ||
0, 1, 0, | ||
]) | ||
positions: { | ||
value: new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), | ||
size: 2 | ||
} | ||
} | ||
@@ -129,42 +85,4 @@ }), | ||
} | ||
_shouldUseMinMax() { | ||
const { minColor, maxColor, colorDomain, colorRange } = this.props; | ||
if (minColor || maxColor) { | ||
log.deprecated('ScreenGridLayer props: minColor and maxColor', 'colorRange, colorDomain')(); | ||
return true; | ||
} | ||
// minColor and maxColor not supplied, check if colorRange or colorDomain supplied. | ||
// NOTE: colorDomain and colorRange are experimental features, use them only when supplied. | ||
if (colorDomain || colorRange) { | ||
return false; | ||
} | ||
// None specified, use default minColor and maxColor | ||
return true; | ||
} | ||
_updateUniforms(oldProps, props, changeFlags) { | ||
const model = this.state.model; | ||
if (COLOR_PROPS.some(key => oldProps[key] !== props[key])) { | ||
model.setUniforms({ shouldUseMinMax: this._shouldUseMinMax() }); | ||
} | ||
if (oldProps.colorRange !== props.colorRange) { | ||
model.setUniforms({ colorRange: colorRangeToFlatArray(props.colorRange) }); | ||
} | ||
if (oldProps.cellMarginPixels !== props.cellMarginPixels || | ||
oldProps.cellSizePixels !== props.cellSizePixels || | ||
changeFlags.viewportChanged) { | ||
const { width, height } = this.context.viewport; | ||
const { cellSizePixels, cellMarginPixels } = this.props; | ||
const margin = cellSizePixels > cellMarginPixels ? cellMarginPixels : 0; | ||
const cellScale = new Float32Array([ | ||
((cellSizePixels - margin) / width) * 2, | ||
(-(cellSizePixels - margin) / height) * 2, | ||
1 | ||
]); | ||
// @ts-expect-error stricter luma gl types | ||
model.setUniforms({ cellScale }); | ||
} | ||
} | ||
} | ||
ScreenGridCellLayer.layerName = 'ScreenGridCellLayer'; | ||
ScreenGridCellLayer.defaultProps = defaultProps; | ||
export default ScreenGridCellLayer; |
@@ -1,3 +0,3 @@ | ||
declare const _default: "#version 300 es\n#define SHADER_NAME screen-grid-layer-fragment-shader\n\nprecision highp float;\n\nin vec4 vColor;\nin float vSampleCount;\n\nout vec4 fragColor;\n\nvoid main(void) {\n if (vSampleCount <= 0.0) {\n discard;\n }\n fragColor = vColor;\n\n DECKGL_FILTER_COLOR(fragColor, geometry);\n}\n"; | ||
declare const _default: "#version 300 es\n#define SHADER_NAME screen-grid-layer-fragment-shader\n\nprecision highp float;\n\nin vec4 vColor;\n\nout vec4 fragColor;\n\nvoid main(void) {\n fragColor = vColor;\n\n DECKGL_FILTER_COLOR(fragColor, geometry);\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=screen-grid-layer-fragment.glsl.d.ts.map |
@@ -1,22 +0,6 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
/* fragment shader for the grid-layer */ | ||
export default `\ | ||
export default /* glsl */ `\ | ||
#version 300 es | ||
@@ -26,8 +10,4 @@ #define SHADER_NAME screen-grid-layer-fragment-shader | ||
in vec4 vColor; | ||
in float vSampleCount; | ||
out vec4 fragColor; | ||
void main(void) { | ||
if (vSampleCount <= 0.0) { | ||
discard; | ||
} | ||
fragColor = vColor; | ||
@@ -34,0 +14,0 @@ DECKGL_FILTER_COLOR(fragColor, geometry); |
@@ -1,3 +0,3 @@ | ||
declare const _default: "#version 300 es\n#define SHADER_NAME screen-grid-layer-vertex-shader\n#define RANGE_COUNT 6\n\nin vec3 positions;\nin vec3 instancePositions;\nin vec4 instanceCounts;\nin vec3 instancePickingColors;\n\nuniform float opacity;\nuniform vec3 cellScale;\nuniform vec4 minColor;\nuniform vec4 maxColor;\nuniform vec4 colorRange[RANGE_COUNT];\nuniform vec2 colorDomain;\nuniform bool shouldUseMinMax;\nuniform sampler2D maxTexture;\n\nout vec4 vColor;\nout float vSampleCount;\n\nvec4 quantizeScale(vec2 domain, vec4 range[RANGE_COUNT], float value) {\n vec4 outColor = vec4(0., 0., 0., 0.);\n if (value >= domain.x && value <= domain.y) {\n float domainRange = domain.y - domain.x;\n if (domainRange <= 0.) {\n outColor = colorRange[0];\n } else {\n float rangeCount = float(RANGE_COUNT);\n float rangeStep = domainRange / rangeCount;\n float idx = floor((value - domain.x) / rangeStep);\n idx = clamp(idx, 0., rangeCount - 1.);\n int intIdx = int(idx);\n outColor = colorRange[intIdx];\n }\n }\n outColor = outColor / 255.;\n return outColor;\n}\n\nvoid main(void) {\n vSampleCount = instanceCounts.a;\n\n float weight = instanceCounts.r;\n float maxWeight = texture(maxTexture, vec2(0.5)).r;\n\n float step = weight / maxWeight;\n vec4 minMaxColor = mix(minColor, maxColor, step) / 255.;\n\n vec2 domain = colorDomain;\n float domainMaxValid = float(colorDomain.y != 0.);\n domain.y = mix(maxWeight, colorDomain.y, domainMaxValid);\n vec4 rangeColor = quantizeScale(domain, colorRange, weight);\n\n float rangeMinMax = float(shouldUseMinMax);\n vec4 color = mix(rangeColor, minMaxColor, rangeMinMax);\n vColor = vec4(color.rgb, color.a * opacity);\n\n // Set color to be rendered to picking fbo (also used to check for selection highlight).\n picking_setPickingColor(instancePickingColors);\n\n gl_Position = vec4(instancePositions + positions * cellScale, 1.);\n}\n"; | ||
declare const _default: "#version 300 es\n#define SHADER_NAME screen-grid-layer-vertex-shader\n#define RANGE_COUNT 6\n\nin vec2 positions;\nin vec2 instancePositions;\nin float instanceWeights;\nin vec3 instancePickingColors;\n\nuniform sampler2D colorRange;\n\nout vec4 vColor;\n\nvec4 interp(float value, vec2 domain, sampler2D range) {\n float r = (value - domain.x) / (domain.y - domain.x);\n return texture(range, vec2(r, 0.5));\n}\n\nvoid main(void) {\n if (isnan(instanceWeights)) {\n gl_Position = vec4(0.);\n return;\n }\n\n vec2 pos = instancePositions * screenGrid.gridSizeClipspace + positions * screenGrid.cellSizeClipspace;\n pos.x = pos.x - 1.0;\n pos.y = 1.0 - pos.y;\n\n gl_Position = vec4(pos, 0., 1.);\n\n vColor = interp(instanceWeights, screenGrid.colorDomain, colorRange);\n vColor.a *= layer.opacity;\n\n // Set color to be rendered to picking fbo (also used to check for selection highlight).\n picking_setPickingColor(instancePickingColors);\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=screen-grid-layer-vertex.glsl.d.ts.map |
@@ -1,72 +0,31 @@ | ||
// 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. | ||
export default `\ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default /* glsl */ `\ | ||
#version 300 es | ||
#define SHADER_NAME screen-grid-layer-vertex-shader | ||
#define RANGE_COUNT 6 | ||
in vec3 positions; | ||
in vec3 instancePositions; | ||
in vec4 instanceCounts; | ||
in vec2 positions; | ||
in vec2 instancePositions; | ||
in float instanceWeights; | ||
in vec3 instancePickingColors; | ||
uniform float opacity; | ||
uniform vec3 cellScale; | ||
uniform vec4 minColor; | ||
uniform vec4 maxColor; | ||
uniform vec4 colorRange[RANGE_COUNT]; | ||
uniform vec2 colorDomain; | ||
uniform bool shouldUseMinMax; | ||
uniform sampler2D maxTexture; | ||
uniform sampler2D colorRange; | ||
out vec4 vColor; | ||
out float vSampleCount; | ||
vec4 quantizeScale(vec2 domain, vec4 range[RANGE_COUNT], float value) { | ||
vec4 outColor = vec4(0., 0., 0., 0.); | ||
if (value >= domain.x && value <= domain.y) { | ||
float domainRange = domain.y - domain.x; | ||
if (domainRange <= 0.) { | ||
outColor = colorRange[0]; | ||
} else { | ||
float rangeCount = float(RANGE_COUNT); | ||
float rangeStep = domainRange / rangeCount; | ||
float idx = floor((value - domain.x) / rangeStep); | ||
idx = clamp(idx, 0., rangeCount - 1.); | ||
int intIdx = int(idx); | ||
outColor = colorRange[intIdx]; | ||
vec4 interp(float value, vec2 domain, sampler2D range) { | ||
float r = (value - domain.x) / (domain.y - domain.x); | ||
return texture(range, vec2(r, 0.5)); | ||
} | ||
void main(void) { | ||
if (isnan(instanceWeights)) { | ||
gl_Position = vec4(0.); | ||
return; | ||
} | ||
outColor = outColor / 255.; | ||
return outColor; | ||
} | ||
void main(void) { | ||
vSampleCount = instanceCounts.a; | ||
float weight = instanceCounts.r; | ||
float maxWeight = texture(maxTexture, vec2(0.5)).r; | ||
float step = weight / maxWeight; | ||
vec4 minMaxColor = mix(minColor, maxColor, step) / 255.; | ||
vec2 domain = colorDomain; | ||
float domainMaxValid = float(colorDomain.y != 0.); | ||
domain.y = mix(maxWeight, colorDomain.y, domainMaxValid); | ||
vec4 rangeColor = quantizeScale(domain, colorRange, weight); | ||
float rangeMinMax = float(shouldUseMinMax); | ||
vec4 color = mix(rangeColor, minMaxColor, rangeMinMax); | ||
vColor = vec4(color.rgb, color.a * opacity); | ||
vec2 pos = instancePositions * screenGrid.gridSizeClipspace + positions * screenGrid.cellSizeClipspace; | ||
pos.x = pos.x - 1.0; | ||
pos.y = 1.0 - pos.y; | ||
gl_Position = vec4(pos, 0., 1.); | ||
vColor = interp(instanceWeights, screenGrid.colorDomain, colorRange); | ||
vColor.a *= layer.opacity; | ||
picking_setPickingColor(instancePickingColors); | ||
gl_Position = vec4(instancePositions + positions * cellScale, 1.); | ||
} | ||
`; |
@@ -1,6 +0,6 @@ | ||
import { Accessor, Color, GetPickingInfoParams, Layer, LayerContext, LayersList, PickingInfo, Position, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import type { Buffer, Texture } from '@luma.gl/core'; | ||
import GridAggregationLayer, { GridAggregationLayerProps } from "../grid-aggregation-layer.js"; | ||
import { Accessor, Color, GetPickingInfoParams, CompositeLayerProps, Layer, LayersList, PickingInfo, Position, UpdateParameters, DefaultProps } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator, AggregationOperation } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
/** All properties supported by ScreenGridLayer. */ | ||
export type ScreenGridLayerProps<DataT = unknown> = _ScreenGridLayerProps<DataT> & GridAggregationLayerProps<DataT>; | ||
export type ScreenGridLayerProps<DataT = unknown> = _ScreenGridLayerProps<DataT> & CompositeLayerProps; | ||
/** Properties added by ScreenGridLayer. */ | ||
@@ -19,14 +19,2 @@ export type _ScreenGridLayerProps<DataT> = { | ||
/** | ||
* Expressed as an rgba array, minimal color that could be rendered by a tile. | ||
* @default [0, 0, 0, 255] | ||
* @deprecated Deprecated in version 5.2.0, use `colorRange` and `colorDomain` instead. | ||
*/ | ||
minColor?: Color | null; | ||
/** | ||
* Expressed as an rgba array, maximal color that could be rendered by a tile. | ||
* @default [0, 255, 0, 255] | ||
* @deprecated Deprecated in version 5.2.0, use `colorRange` and `colorDomain` instead. | ||
*/ | ||
maxColor?: Color | null; | ||
/** | ||
* Color scale input domain. The color scale maps continues numeric domain into discrete color range. | ||
@@ -43,2 +31,8 @@ * @default [1, max(weight)] | ||
/** | ||
* Scaling function used to determine the color of the grid cell. | ||
* Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'. | ||
* @default 'quantize' | ||
*/ | ||
colorScaleType?: 'linear' | 'quantize'; | ||
/** | ||
* Method called to retrieve the position of each object. | ||
@@ -63,36 +57,35 @@ * | ||
* Defines the type of aggregation operation | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* V valid values are 'SUM', 'MEAN', 'MIN' and 'MAX'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
aggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
aggregation?: AggregationOperation; | ||
}; | ||
export type ScreenGridLayerPickingInfo<DataT> = PickingInfo<{ | ||
/** Column index of the picked cell, starting from 0 at the left of the viewport */ | ||
col: number; | ||
/** Row index of the picked cell, starting from 0 at the top of the viewport */ | ||
row: number; | ||
/** Aggregated value */ | ||
value: number; | ||
/** Number of data points in the picked cell */ | ||
count: number; | ||
/** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */ | ||
pointIndices?: number[]; | ||
/** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */ | ||
points?: DataT[]; | ||
}>; | ||
/** Aggregates data into histogram bins and renders them as a grid. */ | ||
export default class ScreenGridLayer<DataT = any, ExtraProps extends {} = {}> extends GridAggregationLayer<DataT, ExtraProps & Required<_ScreenGridLayerProps<DataT>>> { | ||
export default class ScreenGridLayer<DataT = any, ExtraProps extends {} = {}> extends AggregationLayer<DataT, ExtraProps & Required<_ScreenGridLayerProps<DataT>>> { | ||
static layerName: string; | ||
static defaultProps: DefaultProps<ScreenGridLayerProps<unknown>>; | ||
state: GridAggregationLayer<DataT>['state'] & { | ||
supported: boolean; | ||
gpuGridAggregator?: any; | ||
gpuAggregation?: any; | ||
weights?: any; | ||
maxTexture?: Texture; | ||
aggregationBuffer?: Buffer; | ||
maxBuffer?: Buffer; | ||
}; | ||
getAggregatorType(): string; | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator; | ||
initializeState(): void; | ||
shouldUpdateState({ changeFlags }: UpdateParameters<this>): boolean; | ||
updateState(opts: UpdateParameters<this>): void; | ||
renderLayers(): LayersList | Layer; | ||
finalizeState(context: LayerContext): void; | ||
getPickingInfo({ info }: GetPickingInfoParams): PickingInfo; | ||
updateResults({ aggregationData, maxData }: { | ||
aggregationData: any; | ||
maxData: any; | ||
}): void; | ||
updateAggregationState(opts: any): void; | ||
_updateAccessors(opts: any): void; | ||
_resetResults(): void; | ||
updateState(params: UpdateParameters<this>): boolean; | ||
onAttributeChange(id: string): void; | ||
renderLayers(): LayersList | Layer | null; | ||
getPickingInfo(params: GetPickingInfoParams): ScreenGridLayerPickingInfo<DataT>; | ||
} | ||
//# sourceMappingURL=screen-grid-layer.d.ts.map |
@@ -1,72 +0,76 @@ | ||
// 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 { log } from '@deck.gl/core'; | ||
import GPUGridAggregator from "../utils/gpu-grid-aggregation/gpu-grid-aggregator.js"; | ||
import { AGGREGATION_OPERATION, getValueFunc } from "../utils/aggregation-operation-utils.js"; | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { project32 } from '@deck.gl/core'; | ||
import { WebGLAggregator, CPUAggregator } from "../common/aggregator/index.js"; | ||
import AggregationLayer from "../common/aggregation-layer.js"; | ||
import ScreenGridCellLayer from "./screen-grid-cell-layer.js"; | ||
import GridAggregationLayer from "../grid-aggregation-layer.js"; | ||
import { getFloatTexture } from "../utils/resource-utils.js"; | ||
import { binOptionsUniforms } from "./bin-options-uniforms.js"; | ||
import { defaultColorRange } from "../common/utils/color-utils.js"; | ||
const defaultProps = { | ||
...ScreenGridCellLayer.defaultProps, | ||
cellSizePixels: { type: 'number', value: 100, min: 1 }, | ||
cellMarginPixels: { type: 'number', value: 2, min: 0 }, | ||
colorRange: defaultColorRange, | ||
colorScaleType: 'linear', | ||
getPosition: { type: 'accessor', value: (d) => d.position }, | ||
getWeight: { type: 'accessor', value: 1 }, | ||
gpuAggregation: false, // TODO(v9): Re-enable GPU aggregation. | ||
gpuAggregation: false, | ||
aggregation: 'SUM' | ||
}; | ||
const POSITION_ATTRIBUTE_NAME = 'positions'; | ||
const DIMENSIONS = { | ||
data: { | ||
props: ['cellSizePixels'] | ||
}, | ||
weights: { | ||
props: ['aggregation'], | ||
accessors: ['getWeight'] | ||
/** Aggregates data into histogram bins and renders them as a grid. */ | ||
class ScreenGridLayer extends AggregationLayer { | ||
getAggregatorType() { | ||
return this.props.gpuAggregation && WebGLAggregator.isSupported(this.context.device) | ||
? 'gpu' | ||
: 'cpu'; | ||
} | ||
}; | ||
/** Aggregates data into histogram bins and renders them as a grid. */ | ||
class ScreenGridLayer extends GridAggregationLayer { | ||
createAggregator(type) { | ||
if (type === 'cpu' || !WebGLAggregator.isSupported(this.context.device)) { | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({ positions }, index, opts) => { | ||
const viewport = this.context.viewport; | ||
const p = viewport.project(positions); | ||
const cellSizePixels = opts.cellSizePixels; | ||
if (p[0] < 0 || p[0] >= viewport.width || p[1] < 0 || p[1] >= viewport.height) { | ||
// Not on screen | ||
return null; | ||
} | ||
return [Math.floor(p[0] / cellSizePixels), Math.floor(p[1] / cellSizePixels)]; | ||
} | ||
}, | ||
getValue: [{ sources: ['counts'], getValue: ({ counts }) => counts }] | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 1, | ||
bufferLayout: this.getAttributeManager().getBufferLayouts({ isInstanced: false }), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float counts; | ||
void getBin(out ivec2 binId) { | ||
vec4 pos = project_position_to_clipspace(positions, positions64Low, vec3(0.0)); | ||
vec2 screenCoords = vec2(pos.x / pos.w + 1.0, 1.0 - pos.y / pos.w) / 2.0 * project.viewportSize / project.devicePixelRatio; | ||
vec2 gridCoords = floor(screenCoords / binOptions.cellSizePixels); | ||
binId = ivec2(gridCoords); | ||
} | ||
void getValue(out float weight) { | ||
weight = counts; | ||
} | ||
` | ||
}) | ||
}); | ||
} | ||
initializeState() { | ||
super.initializeAggregationLayer({ | ||
dimensions: DIMENSIONS, | ||
// @ts-expect-error | ||
getCellSize: props => props.cellSizePixels // TODO | ||
}); | ||
const weights = { | ||
count: { | ||
size: 1, | ||
operation: AGGREGATION_OPERATION.SUM, | ||
needMax: true, | ||
maxTexture: getFloatTexture(this.context.device, { id: `${this.id}-max-texture` }) | ||
} | ||
}; | ||
this.setState({ | ||
supported: true, | ||
projectPoints: true, // aggregation in screen space | ||
weights, | ||
subLayerData: { attributes: {} }, | ||
maxTexture: weights.count.maxTexture, | ||
positionAttributeName: 'positions', | ||
posOffset: [0, 0], | ||
translation: [1, -1] | ||
}); | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager(); | ||
attributeManager.add({ | ||
[POSITION_ATTRIBUTE_NAME]: { | ||
positions: { | ||
size: 3, | ||
@@ -78,131 +82,110 @@ accessor: 'getPosition', | ||
// this attribute is used in gpu aggregation path only | ||
count: { size: 3, accessor: 'getWeight' } | ||
counts: { size: 1, accessor: 'getWeight' } | ||
}); | ||
} | ||
shouldUpdateState({ changeFlags }) { | ||
return this.state.supported && changeFlags.somethingChanged; | ||
return changeFlags.somethingChanged; | ||
} | ||
updateState(opts) { | ||
super.updateState(opts); | ||
updateState(params) { | ||
const aggregatorChanged = super.updateState(params); | ||
const { props, oldProps, changeFlags } = params; | ||
const { cellSizePixels, aggregation } = props; | ||
if (aggregatorChanged || | ||
changeFlags.dataChanged || | ||
changeFlags.updateTriggersChanged || | ||
changeFlags.viewportChanged || | ||
aggregation !== oldProps.aggregation || | ||
cellSizePixels !== oldProps.cellSizePixels) { | ||
const { width, height } = this.context.viewport; | ||
const { aggregator } = this.state; | ||
if (aggregator instanceof WebGLAggregator) { | ||
aggregator.setProps({ | ||
binIdRange: [ | ||
[0, Math.ceil(width / cellSizePixels)], | ||
[0, Math.ceil(height / cellSizePixels)] | ||
] | ||
}); | ||
} | ||
aggregator.setProps({ | ||
pointCount: this.getNumInstances(), | ||
operations: [aggregation], | ||
binOptions: { | ||
cellSizePixels | ||
} | ||
}); | ||
} | ||
if (changeFlags.viewportChanged) { | ||
// Rerun aggregation on viewport change | ||
this.state.aggregator.setNeedsUpdate(); | ||
} | ||
return aggregatorChanged; | ||
} | ||
onAttributeChange(id) { | ||
const { aggregator } = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
break; | ||
case 'counts': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
} | ||
renderLayers() { | ||
if (!this.state.supported) { | ||
return []; | ||
} | ||
const { maxTexture, numRow, numCol, weights } = this.state; | ||
const { updateTriggers } = this.props; | ||
const { aggregationBuffer } = weights.count; | ||
const { aggregator } = this.state; | ||
const CellLayerClass = this.getSubLayerClass('cells', ScreenGridCellLayer); | ||
const binAttribute = aggregator.getBins(); | ||
const weightAttribute = aggregator.getResult(0); | ||
return new CellLayerClass(this.props, this.getSubLayerProps({ | ||
id: 'cell-layer', | ||
updateTriggers | ||
id: 'cell-layer' | ||
}), { | ||
data: { attributes: { instanceCounts: aggregationBuffer } }, | ||
maxTexture, | ||
numInstances: numRow * numCol | ||
data: { | ||
length: aggregator.binCount, | ||
attributes: { | ||
getBin: binAttribute, | ||
getWeight: weightAttribute | ||
} | ||
}, | ||
// Data has changed shallowly, but we likely don't need to update the attributes | ||
dataComparator: (data, oldData) => data.length === oldData.length, | ||
updateTriggers: { | ||
getBin: [binAttribute], | ||
getWeight: [weightAttribute] | ||
}, | ||
parameters: { | ||
depthWriteEnabled: false, | ||
...this.props.parameters | ||
}, | ||
// Evaluate domain at draw() time | ||
colorDomain: () => this.props.colorDomain || aggregator.getResultDomain(0), | ||
// Extensions are already handled by the GPUAggregator, do not pass it down | ||
extensions: [] | ||
}); | ||
} | ||
finalizeState(context) { | ||
super.finalizeState(context); | ||
const { aggregationBuffer, maxBuffer, maxTexture } = this.state; | ||
aggregationBuffer?.delete(); | ||
maxBuffer?.delete(); | ||
maxTexture?.delete(); | ||
} | ||
getPickingInfo({ info }) { | ||
getPickingInfo(params) { | ||
const info = params.info; | ||
const { index } = info; | ||
if (index >= 0) { | ||
const { gpuGridAggregator, gpuAggregation, weights } = this.state; | ||
// Get count aggregation results | ||
const aggregationResults = gpuAggregation | ||
? gpuGridAggregator.getData('count') | ||
: weights.count; | ||
// Each instance (one cell) is aggregated into single pixel, | ||
// Get current instance's aggregation details. | ||
info.object = GPUGridAggregator.getAggregationData({ | ||
pixelIndex: index, | ||
...aggregationResults | ||
}); | ||
const bin = this.state.aggregator.getBin(index); | ||
let object; | ||
if (bin) { | ||
object = { | ||
col: bin.id[0], | ||
row: bin.id[1], | ||
value: bin.value[0], | ||
count: bin.count | ||
}; | ||
if (bin.pointIndices) { | ||
object.pointIndices = bin.pointIndices; | ||
object.points = Array.isArray(this.props.data) | ||
? bin.pointIndices.map(i => this.props.data[i]) | ||
: []; | ||
} | ||
} | ||
info.object = object; | ||
} | ||
return info; | ||
} | ||
// Aggregation Overrides | ||
updateResults({ aggregationData, maxData }) { | ||
const { count } = this.state.weights; | ||
count.aggregationData = aggregationData; | ||
count.aggregationBuffer.write(aggregationData); | ||
count.maxData = maxData; | ||
count.maxTexture.setImageData({ data: maxData }); | ||
} | ||
/* eslint-disable complexity, max-statements */ | ||
updateAggregationState(opts) { | ||
const cellSize = opts.props.cellSizePixels; | ||
const cellSizeChanged = opts.oldProps.cellSizePixels !== cellSize; | ||
const { viewportChanged } = opts.changeFlags; | ||
let gpuAggregation = opts.props.gpuAggregation; | ||
if (this.state.gpuAggregation !== opts.props.gpuAggregation) { | ||
if (gpuAggregation && !GPUGridAggregator.isSupported(this.context.device)) { | ||
log.warn('GPU Grid Aggregation not supported, falling back to CPU')(); | ||
gpuAggregation = false; | ||
} | ||
} | ||
const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; | ||
this.setState({ | ||
gpuAggregation | ||
}); | ||
const positionsChanged = this.isAttributeChanged(POSITION_ATTRIBUTE_NAME); | ||
const { dimensions } = this.state; | ||
const { data, weights } = dimensions; | ||
const aggregationDataDirty = positionsChanged || | ||
gpuAggregationChanged || | ||
viewportChanged || | ||
this.isAggregationDirty(opts, { | ||
compareAll: gpuAggregation, // check for all (including extentions props) when using gpu aggregation | ||
dimension: data | ||
}); | ||
const aggregationWeightsDirty = this.isAggregationDirty(opts, { dimension: weights }); | ||
this.setState({ | ||
aggregationDataDirty, | ||
aggregationWeightsDirty | ||
}); | ||
const { viewport } = this.context; | ||
if (viewportChanged || cellSizeChanged) { | ||
const { width, height } = viewport; | ||
const numCol = Math.ceil(width / cellSize); | ||
const numRow = Math.ceil(height / cellSize); | ||
this.allocateResources(numRow, numCol); | ||
this.setState({ | ||
// transformation from clipspace to screen(pixel) space | ||
scaling: [width / 2, -height / 2, 1], | ||
gridOffset: { xOffset: cellSize, yOffset: cellSize }, | ||
width, | ||
height, | ||
numCol, | ||
numRow | ||
}); | ||
} | ||
if (aggregationWeightsDirty) { | ||
this._updateAccessors(opts); | ||
} | ||
if (aggregationDataDirty || aggregationWeightsDirty) { | ||
this._resetResults(); | ||
} | ||
} | ||
/* eslint-enable complexity, max-statements */ | ||
// Private | ||
_updateAccessors(opts) { | ||
const { getWeight, aggregation, data } = opts.props; | ||
const { count } = this.state.weights; | ||
if (count) { | ||
count.getWeight = getWeight; | ||
count.operation = AGGREGATION_OPERATION[aggregation]; | ||
} | ||
this.setState({ getValue: getValueFunc(aggregation, getWeight, { data }) }); | ||
} | ||
_resetResults() { | ||
const { count } = this.state.weights; | ||
if (count) { | ||
count.aggregationData = null; | ||
} | ||
} | ||
} | ||
@@ -209,0 +192,0 @@ ScreenGridLayer.layerName = 'ScreenGridLayer'; |
@@ -6,3 +6,3 @@ { | ||
"type": "module", | ||
"version": "9.0.35", | ||
"version": "9.1.0-beta.1", | ||
"publishConfig": { | ||
@@ -42,14 +42,15 @@ "access": "public" | ||
"dependencies": { | ||
"@luma.gl/constants": "~9.0.27", | ||
"@luma.gl/shadertools": "~9.0.27", | ||
"@math.gl/web-mercator": "^4.0.0", | ||
"@luma.gl/constants": "^9.1.0-beta.11", | ||
"@luma.gl/shadertools": "^9.1.0-beta.11", | ||
"@math.gl/core": "^4.1.0", | ||
"@math.gl/web-mercator": "^4.1.0", | ||
"d3-hexbin": "^0.2.1" | ||
}, | ||
"peerDependencies": { | ||
"@deck.gl/core": "^9.0.0", | ||
"@deck.gl/layers": "^9.0.0", | ||
"@luma.gl/core": "~9.0.0", | ||
"@luma.gl/engine": "~9.0.0" | ||
"@deck.gl/core": "9.0.0-alpha.0", | ||
"@deck.gl/layers": "9.0.0-alpha.0", | ||
"@luma.gl/core": "^9.1.0-beta.11", | ||
"@luma.gl/engine": "^9.1.0-beta.11" | ||
}, | ||
"gitHead": "8e8fc65d8fa9b9bf7e6d82a7e89d37c6d0126aae" | ||
"gitHead": "a86fb154cf323eacc3f56ef087f636fb9f6c30b6" | ||
} |
@@ -1,49 +0,38 @@ | ||
// Copyright (c) 2015 - 2018 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import {LineLayer, SolidPolygonLayer} from '@deck.gl/layers'; | ||
import {generateContours} from './contour-utils'; | ||
import { | ||
Accessor, | ||
AccessorFunction, | ||
Color, | ||
log, | ||
COORDINATE_SYSTEM, | ||
GetPickingInfoParams, | ||
project32, | ||
LayersList, | ||
PickingInfo, | ||
Position, | ||
Viewport, | ||
_deepEqual, | ||
UpdateParameters, | ||
DefaultProps, | ||
LayersList | ||
DefaultProps | ||
} from '@deck.gl/core'; | ||
import {PathLayer, SolidPolygonLayer} from '@deck.gl/layers'; | ||
import {WebGLAggregator, CPUAggregator, AggregationOperation} from '../common/aggregator/index'; | ||
import AggregationLayer from '../common/aggregation-layer'; | ||
import {AggregationLayerProps} from '../common/aggregation-layer'; | ||
import {generateContours, Contour, ContourLine, ContourPolygon} from './contour-utils'; | ||
import {getAggregatorValueReader} from './value-reader'; | ||
import {getBinIdRange} from '../common/utils/bounds-utils'; | ||
import {Matrix4} from '@math.gl/core'; | ||
import {BinOptions, binOptionsUniforms} from './bin-options-uniforms'; | ||
import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator'; | ||
import {AGGREGATION_OPERATION, getValueFunc} from '../utils/aggregation-operation-utils'; | ||
import {getBoundingBox, getGridParams} from '../utils/grid-aggregation-utils'; | ||
import GridAggregationLayer, {GridAggregationLayerProps} from '../grid-aggregation-layer'; | ||
const DEFAULT_COLOR = [255, 255, 255, 255]; | ||
const DEFAULT_STROKE_WIDTH = 1; | ||
const DEFAULT_THRESHOLD = 1; | ||
const defaultProps: DefaultProps<ContourLayerProps> = { | ||
// grid aggregation | ||
cellSize: {type: 'number', min: 1, max: 1000, value: 1000}, | ||
cellSize: {type: 'number', min: 1, value: 1000}, | ||
gridOrigin: {type: 'array', compare: true, value: [0, 0]}, | ||
getPosition: {type: 'accessor', value: (x: any) => x.position}, | ||
getWeight: {type: 'accessor', value: 1}, | ||
gpuAggregation: false, // TODO(v9): Re-enable GPU aggregation. | ||
gpuAggregation: true, | ||
aggregation: 'SUM', | ||
@@ -54,3 +43,3 @@ | ||
type: 'object', | ||
value: [{threshold: DEFAULT_THRESHOLD}], | ||
value: [{threshold: 1}], | ||
optional: true, | ||
@@ -63,20 +52,8 @@ compare: 3 | ||
const POSITION_ATTRIBUTE_NAME = 'positions'; | ||
const DIMENSIONS = { | ||
data: { | ||
props: ['cellSize'] | ||
}, | ||
weights: { | ||
props: ['aggregation'], | ||
accessors: ['getWeight'] | ||
} | ||
}; | ||
/** All properties supported by ContourLayer. */ | ||
/** All properties supported by GridLayer. */ | ||
export type ContourLayerProps<DataT = unknown> = _ContourLayerProps<DataT> & | ||
GridAggregationLayerProps<DataT>; | ||
AggregationLayerProps<DataT>; | ||
/** Properties added by ContourLayer. */ | ||
export type _ContourLayerProps<DataT> = { | ||
/** Properties added by GridLayer. */ | ||
type _ContourLayerProps<DataT> = { | ||
/** | ||
@@ -89,4 +66,10 @@ * Size of each cell in meters. | ||
/** | ||
* The grid origin | ||
* @default [0, 0] | ||
*/ | ||
gridOrigin?: [number, number]; | ||
/** | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
* @default true | ||
* @default false | ||
*/ | ||
@@ -99,3 +82,3 @@ gpuAggregation?: boolean; | ||
*/ | ||
aggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
aggregation?: AggregationOperation; | ||
@@ -106,26 +89,4 @@ /** | ||
*/ | ||
contours: { | ||
/** | ||
* Isolines: `threshold` value must be a single `Number`, Isolines are generated based on this threshold value. | ||
* | ||
* Isobands: `threshold` value must be an Array of two `Number`s. Isobands are generated using `[threshold[0], threshold[1])` as threshold range, i.e area that has values `>= threshold[0]` and `< threshold[1]` are rendered with corresponding color. NOTE: `threshold[0]` is inclusive and `threshold[1]` is not inclusive. | ||
*/ | ||
threshold: number | number[]; | ||
contours?: Contour[]; | ||
/** | ||
* RGBA color array to be used to render the contour. | ||
* @default [255, 255, 255, 255] | ||
*/ | ||
color?: Color; | ||
/** | ||
* Applicable for `Isoline`s only, width of the Isoline in pixels. | ||
* @default 1 | ||
*/ | ||
strokeWidth?: number; | ||
/** Defines z order of the contour. */ | ||
zIndex?: number; | ||
}[]; | ||
/** | ||
@@ -141,3 +102,3 @@ * A very small z offset that is added for each vertex of a contour (Isoline or Isoband). | ||
*/ | ||
getPosition?: AccessorFunction<DataT, Position>; | ||
getPosition?: Accessor<DataT, Position>; | ||
@@ -151,42 +112,85 @@ /** | ||
/** Aggregate data into iso-lines or iso-bands for a given threshold and cell size. */ | ||
export default class ContourLayer< | ||
DataT = any, | ||
ExtraPropsT extends {} = {} | ||
> extends GridAggregationLayer<DataT, ExtraPropsT & Required<_ContourLayerProps<DataT>>> { | ||
export type ContourLayerPickingInfo = PickingInfo<{ | ||
contour: Contour; | ||
}>; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends AggregationLayer< | ||
DataT, | ||
ExtraPropsT & Required<_ContourLayerProps<DataT>> | ||
> { | ||
static layerName = 'ContourLayer'; | ||
static defaultProps = defaultProps; | ||
state!: GridAggregationLayer<DataT>['state'] & { | ||
contourData: { | ||
contourSegments: { | ||
start: number[]; | ||
end: number[]; | ||
contour: any; | ||
}[]; | ||
contourPolygons: { | ||
vertices: number[][]; | ||
contour: any; | ||
}[]; | ||
state!: AggregationLayer<DataT>['state'] & | ||
BinOptions & { | ||
// Aggregator result | ||
aggregatedValueReader?: (x: number, y: number) => number; | ||
contourData?: { | ||
lines: ContourLine[]; | ||
polygons: ContourPolygon[]; | ||
}; | ||
binIdRange: [number, number][]; | ||
aggregatorViewport: Viewport; | ||
}; | ||
thresholdData: any; | ||
}; | ||
initializeState(): void { | ||
super.initializeAggregationLayer({ | ||
dimensions: DIMENSIONS | ||
getAggregatorType(): string { | ||
return this.props.gpuAggregation && WebGLAggregator.isSupported(this.context.device) | ||
? 'gpu' | ||
: 'cpu'; | ||
} | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator { | ||
if (type === 'cpu') { | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({positions}: {positions: number[]}, index: number, opts: BinOptions) => { | ||
const viewport = this.state.aggregatorViewport; | ||
// project to common space | ||
const p = viewport.projectPosition(positions); | ||
const {cellSizeCommon, cellOriginCommon} = opts; | ||
return [ | ||
Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((p[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}, | ||
getValue: [{sources: ['counts'], getValue: ({counts}) => counts}], | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 1, | ||
bufferLayout: this.getAttributeManager()!.getBufferLayouts({isInstanced: false}), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: /* glsl */ ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float counts; | ||
void getBin(out ivec2 binId) { | ||
vec3 positionCommon = project_position(positions, positions64Low); | ||
vec2 gridCoords = floor(positionCommon.xy / binOptions.cellSizeCommon); | ||
binId = ivec2(gridCoords); | ||
} | ||
void getValue(out float value) { | ||
value = counts; | ||
} | ||
` | ||
}), | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
this.setState({ | ||
contourData: {}, | ||
projectPoints: false, | ||
weights: { | ||
count: { | ||
size: 1, | ||
operation: AGGREGATION_OPERATION.SUM | ||
} | ||
} | ||
}); | ||
} | ||
initializeState() { | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager()!; | ||
attributeManager.add({ | ||
[POSITION_ATTRIBUTE_NAME]: { | ||
positions: { | ||
size: 3, | ||
@@ -197,196 +201,227 @@ accessor: 'getPosition', | ||
}, | ||
// this attribute is used in gpu aggregation path only | ||
count: {size: 3, accessor: 'getWeight'} | ||
counts: {size: 1, accessor: 'getWeight'} | ||
}); | ||
} | ||
updateState(opts: UpdateParameters<this>): void { | ||
super.updateState(opts); | ||
let contoursChanged = false; | ||
const {oldProps, props} = opts; | ||
const {aggregationDirty} = this.state; | ||
updateState(params: UpdateParameters<this>) { | ||
const aggregatorChanged = super.updateState(params); | ||
if (oldProps.contours !== props.contours || oldProps.zOffset !== props.zOffset) { | ||
contoursChanged = true; | ||
this._updateThresholdData(opts.props); | ||
const {props, oldProps, changeFlags} = params; | ||
const {aggregator} = this.state; | ||
if ( | ||
aggregatorChanged || | ||
changeFlags.dataChanged || | ||
props.cellSize !== oldProps.cellSize || | ||
!_deepEqual(props.gridOrigin, oldProps.gridOrigin, 1) || | ||
props.aggregation !== oldProps.aggregation | ||
) { | ||
this._updateBinOptions(); | ||
const {cellSizeCommon, cellOriginCommon, binIdRange} = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
pointCount: this.getNumInstances(), | ||
operations: [props.aggregation], | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
} | ||
}); | ||
} | ||
if (this.getNumInstances() > 0 && (aggregationDirty || contoursChanged)) { | ||
this._generateContours(); | ||
if (!_deepEqual(oldProps.contours, props.contours, 2)) { | ||
// Recalculate contours | ||
this.setState({contourData: null}); | ||
} | ||
return aggregatorChanged; | ||
} | ||
renderLayers(): LayersList { | ||
const {contourSegments, contourPolygons} = this.state.contourData; | ||
private _updateBinOptions() { | ||
const bounds = this.getBounds(); | ||
const cellSizeCommon: [number, number] = [1, 1]; | ||
let cellOriginCommon: [number, number] = [0, 0]; | ||
let binIdRange: [number, number][] = [ | ||
[0, 1], | ||
[0, 1] | ||
]; | ||
let viewport = this.context.viewport; | ||
const LinesSubLayerClass = this.getSubLayerClass('lines', LineLayer); | ||
const BandsSubLayerClass = this.getSubLayerClass('bands', SolidPolygonLayer); | ||
if (bounds && Number.isFinite(bounds[0][0])) { | ||
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]; | ||
const {cellSize, gridOrigin} = this.props; | ||
const {unitsPerMeter} = viewport.getDistanceScales(centroid); | ||
cellSizeCommon[0] = unitsPerMeter[0] * cellSize; | ||
cellSizeCommon[1] = unitsPerMeter[1] * cellSize; | ||
// Contour lines layer | ||
const lineLayer = | ||
contourSegments && | ||
contourSegments.length > 0 && | ||
new LinesSubLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'lines' | ||
}), | ||
{ | ||
data: this.state.contourData.contourSegments, | ||
getSourcePosition: d => d.start, | ||
getTargetPosition: d => d.end, | ||
getColor: d => d.contour.color || DEFAULT_COLOR, | ||
getWidth: d => d.contour.strokeWidth || DEFAULT_STROKE_WIDTH | ||
} | ||
); | ||
// Offset common space to center at the origin of the grid cell where the data center is in | ||
// This improves precision without affecting the cell positions | ||
const centroidCommon = viewport.projectFlat(centroid); | ||
cellOriginCommon = [ | ||
Math.floor((centroidCommon[0] - gridOrigin[0]) / cellSizeCommon[0]) * cellSizeCommon[0] + | ||
gridOrigin[0], | ||
Math.floor((centroidCommon[1] - gridOrigin[1]) / cellSizeCommon[1]) * cellSizeCommon[1] + | ||
gridOrigin[1] | ||
]; | ||
centroid = viewport.unprojectFlat(cellOriginCommon); | ||
// Contour bands layer | ||
const bandsLayer = | ||
contourPolygons && | ||
contourPolygons.length > 0 && | ||
new BandsSubLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'bands' | ||
}), | ||
{ | ||
data: this.state.contourData.contourPolygons, | ||
getPolygon: d => d.vertices, | ||
getFillColor: d => d.contour.color || DEFAULT_COLOR | ||
const ViewportType = viewport.constructor as any; | ||
// We construct a viewport for the GPU aggregator's project module | ||
// This viewport is determined by data | ||
// removes arbitrary precision variance that depends on initial view state | ||
viewport = viewport.isGeospatial | ||
? new ViewportType({longitude: centroid[0], latitude: centroid[1], zoom: 12}) | ||
: new Viewport({position: [centroid[0], centroid[1], 0], zoom: 12}); | ||
// Round to the nearest 32-bit float to match CPU and GPU results | ||
cellOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])]; | ||
binIdRange = getBinIdRange({ | ||
dataBounds: bounds, | ||
getBinId: (p: number[]) => { | ||
const positionCommon = viewport.projectFlat(p); | ||
return [ | ||
Math.floor((positionCommon[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((positionCommon[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
); | ||
}); | ||
} | ||
return [lineLayer, bandsLayer]; | ||
this.setState({cellSizeCommon, cellOriginCommon, binIdRange, aggregatorViewport: viewport}); | ||
} | ||
// Aggregation Overrides | ||
override draw(opts) { | ||
// Replaces render time viewport with our own | ||
if (opts.shaderModuleProps.project) { | ||
opts.shaderModuleProps.project.viewport = this.state.aggregatorViewport; | ||
} | ||
super.draw(opts); | ||
} | ||
/* eslint-disable max-statements, complexity */ | ||
updateAggregationState(opts: UpdateParameters<this>) { | ||
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.device)) { | ||
log.warn('GPU Grid Aggregation not supported, falling back to CPU')(); | ||
gpuAggregation = false; | ||
} | ||
} | ||
const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; | ||
private _onAggregationUpdate() { | ||
const {aggregator, binIdRange} = this.state; | ||
this.setState({ | ||
gpuAggregation | ||
aggregatedValueReader: getAggregatorValueReader({aggregator, binIdRange, channel: 0}), | ||
contourData: null | ||
}); | ||
} | ||
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()); | ||
this.setState({boundingBox}); | ||
private _getContours(): { | ||
lines: ContourLine[]; | ||
polygons: ContourPolygon[]; | ||
} | null { | ||
const {aggregatedValueReader} = this.state; | ||
if (!aggregatedValueReader) { | ||
return null; | ||
} | ||
if (positionsChanged || cellSizeChanged) { | ||
const {gridOffset, translation, width, height, numCol, numRow} = getGridParams( | ||
boundingBox, | ||
cellSize, | ||
viewport, | ||
coordinateSystem | ||
); | ||
this.allocateResources(numRow, numCol); | ||
this.setState({ | ||
gridOffset, | ||
boundingBox, | ||
translation, | ||
posOffset: translation.slice(), // Used for CPU aggregation, to offset points | ||
gridOrigin: [-1 * translation[0], -1 * translation[1]], | ||
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 | ||
if (!this.state.contourData) { | ||
const {binIdRange} = this.state; | ||
const {contours} = this.props; | ||
const contourData = generateContours({ | ||
contours, | ||
getValue: aggregatedValueReader, | ||
xRange: binIdRange[0], | ||
yRange: binIdRange[1] | ||
}); | ||
const aggregationWeightsDirty = this.isAggregationDirty(opts, { | ||
dimension: weights | ||
}); | ||
if (aggregationWeightsDirty) { | ||
this._updateAccessors(opts); | ||
this.state.contourData = contourData; | ||
} | ||
if (aggregationDataDirty || aggregationWeightsDirty) { | ||
this._resetResults(); | ||
} | ||
this.setState({ | ||
aggregationDataDirty, | ||
aggregationWeightsDirty | ||
}); | ||
return this.state.contourData; | ||
} | ||
/* eslint-enable max-statements, complexity */ | ||
// Private (Aggregation) | ||
onAttributeChange(id: string) { | ||
const {aggregator} = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
private _updateAccessors(opts: UpdateParameters<this>) { | ||
const {getWeight, aggregation, data} = opts.props; | ||
const {count} = this.state.weights; | ||
if (count) { | ||
count.getWeight = getWeight; | ||
count.operation = AGGREGATION_OPERATION[aggregation]; | ||
this._updateBinOptions(); | ||
const {cellSizeCommon, cellOriginCommon, binIdRange} = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
} | ||
}); | ||
break; | ||
case 'counts': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
this.setState({getValue: getValueFunc(aggregation, getWeight, {data})}); | ||
} | ||
private _resetResults() { | ||
const {count} = this.state.weights; | ||
if (count) { | ||
count.aggregationData = null; | ||
renderLayers(): LayersList | null { | ||
const contourData = this._getContours(); | ||
if (!contourData) { | ||
return null; | ||
} | ||
} | ||
const {lines, polygons} = contourData; | ||
const {zOffset} = this.props; | ||
const {cellOriginCommon, cellSizeCommon} = this.state; | ||
// Private (Contours) | ||
const LinesSubLayerClass = this.getSubLayerClass('lines', PathLayer); | ||
const BandsSubLayerClass = this.getSubLayerClass('bands', SolidPolygonLayer); | ||
const modelMatrix = new Matrix4() | ||
.translate([cellOriginCommon[0], cellOriginCommon[1], 0]) | ||
.scale([cellSizeCommon[0], cellSizeCommon[1], zOffset]); | ||
private _generateContours() { | ||
const {numCol, numRow, gridOrigin, gridOffset, thresholdData} = this.state; | ||
const {count} = this.state.weights; | ||
let {aggregationData} = count; | ||
if (!aggregationData) { | ||
// @ts-ignore | ||
aggregationData = count.aggregationBuffer!.readSyncWebGL() as Float32Array; | ||
count.aggregationData = aggregationData; | ||
} | ||
// Contour lines layer | ||
const lineLayer = | ||
lines && | ||
lines.length > 0 && | ||
new LinesSubLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'lines' | ||
}), | ||
{ | ||
data: lines, | ||
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, | ||
modelMatrix, | ||
getPath: d => d.vertices, | ||
getColor: d => d.contour.color ?? DEFAULT_COLOR, | ||
getWidth: d => d.contour.strokeWidth ?? DEFAULT_STROKE_WIDTH, | ||
widthUnits: 'pixels' | ||
} | ||
); | ||
const {cellWeights} = GPUGridAggregator.getCellData({countsData: aggregationData}); | ||
const contourData = generateContours({ | ||
thresholdData, | ||
cellWeights, | ||
gridSize: [numCol, numRow], | ||
gridOrigin, | ||
cellSize: [gridOffset.xOffset, gridOffset.yOffset] | ||
}); | ||
// Contour bands layer | ||
const bandsLayer = | ||
polygons && | ||
polygons.length > 0 && | ||
new BandsSubLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'bands' | ||
}), | ||
{ | ||
data: polygons, | ||
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, | ||
modelMatrix, | ||
getPolygon: d => d.vertices, | ||
getFillColor: d => d.contour.color ?? DEFAULT_COLOR | ||
} | ||
); | ||
// contourData contains both iso-lines and iso-bands if requested. | ||
this.setState({contourData}); | ||
return [lineLayer, bandsLayer]; | ||
} | ||
private _updateThresholdData(props) { | ||
const {contours, zOffset} = props; | ||
const count = contours.length; | ||
const thresholdData = new Array(count); | ||
for (let i = 0; i < count; i++) { | ||
const contour = contours[i]; | ||
thresholdData[i] = { | ||
contour, | ||
zIndex: contour.zIndex || i, | ||
zOffset | ||
getPickingInfo(params: GetPickingInfoParams): ContourLayerPickingInfo { | ||
const info: ContourLayerPickingInfo = params.info; | ||
const {object} = info; | ||
if (object) { | ||
info.object = { | ||
contour: (object as ContourLine | ContourPolygon).contour | ||
}; | ||
} | ||
this.setState({thresholdData}); | ||
return info; | ||
} | ||
} |
@@ -1,54 +0,85 @@ | ||
import {getCode, getVertices, CONTOUR_TYPE} from './marching-squares'; | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import type {Color} from '@deck.gl/core'; | ||
import {getCode, getLines, getPolygons} from './marching-squares'; | ||
export type Contour = { | ||
/** | ||
* Isolines: `threshold` value must be a single `Number`, Isolines are generated based on this threshold value. | ||
* | ||
* Isobands: `threshold` value must be an Array of two `Number`s. Isobands are generated using `[threshold[0], threshold[1])` as threshold range, i.e area that has values `>= threshold[0]` and `< threshold[1]` are rendered with corresponding color. NOTE: `threshold[0]` is inclusive and `threshold[1]` is not inclusive. | ||
*/ | ||
threshold: number | number[]; | ||
/** | ||
* RGBA color array to be used to render the contour. | ||
* @default [255, 255, 255, 255] | ||
*/ | ||
color?: Color; | ||
/** | ||
* Applicable for `Isoline`s only, width of the Isoline in pixels. | ||
* @default 1 | ||
*/ | ||
strokeWidth?: number; | ||
/** Defines z order of the contour. */ | ||
zIndex?: number; | ||
}; | ||
export type ContourLine = { | ||
vertices: number[][]; | ||
contour: Contour; | ||
}; | ||
export type ContourPolygon = { | ||
vertices: number[][]; | ||
contour: Contour; | ||
}; | ||
// Given all the cell weights, generates contours for each threshold. | ||
/* eslint-disable max-depth */ | ||
export function generateContours({ | ||
thresholdData, | ||
cellWeights, | ||
gridSize, | ||
gridOrigin, | ||
cellSize | ||
contours, | ||
getValue, | ||
xRange, | ||
yRange | ||
}: { | ||
thresholdData: any; | ||
cellWeights: Float32Array; | ||
gridSize: number[]; | ||
gridOrigin: number[]; | ||
cellSize: number[]; | ||
contours: Contour[]; | ||
getValue: (x: number, y: number) => number; | ||
xRange: [number, number]; | ||
yRange: [number, number]; | ||
}) { | ||
const contourSegments: {start: number[]; end: number[]; contour: any}[] = []; | ||
const contourPolygons: {vertices: number[][]; contour: any}[] = []; | ||
const width = gridSize[0]; | ||
const height = gridSize[1]; | ||
const contourLines: ContourLine[] = []; | ||
const contourPolygons: ContourPolygon[] = []; | ||
let segmentIndex = 0; | ||
let polygonIndex = 0; | ||
for (const data of thresholdData) { | ||
const {contour} = data; | ||
for (let i = 0; i < contours.length; i++) { | ||
const contour = contours[i]; | ||
const z = contour.zIndex ?? i; | ||
const {threshold} = contour; | ||
for (let x = -1; x < width; x++) { | ||
for (let y = -1; y < height; y++) { | ||
for (let x = xRange[0] - 1; x < xRange[1]; x++) { | ||
for (let y = yRange[0] - 1; y < yRange[1]; y++) { | ||
// Get the MarchingSquares code based on neighbor cell weights. | ||
const {code, meanCode} = getCode({ | ||
cellWeights, | ||
getValue, | ||
threshold, | ||
x, | ||
y, | ||
width, | ||
height | ||
xRange, | ||
yRange | ||
}); | ||
const opts = { | ||
type: CONTOUR_TYPE.ISO_BANDS, | ||
gridOrigin, | ||
cellSize, | ||
x, | ||
y, | ||
width, | ||
height, | ||
z, | ||
code, | ||
meanCode, | ||
thresholdData: data | ||
meanCode | ||
}; | ||
if (Array.isArray(threshold)) { | ||
opts.type = CONTOUR_TYPE.ISO_BANDS; | ||
const polygons = getVertices(opts) as number[][][]; | ||
// ISO bands | ||
const polygons = getPolygons(opts); | ||
for (const polygon of polygons) { | ||
@@ -61,9 +92,7 @@ contourPolygons[polygonIndex++] = { | ||
} else { | ||
// Get the intersection vertices based on MarchingSquares code. | ||
opts.type = CONTOUR_TYPE.ISO_LINES; | ||
const vertices = getVertices(opts) as number[][]; | ||
for (let i = 0; i < vertices.length; i += 2) { | ||
contourSegments[segmentIndex++] = { | ||
start: vertices[i], | ||
end: vertices[i + 1], | ||
// ISO lines | ||
const path = getLines(opts); | ||
if (path.length > 0) { | ||
contourLines[segmentIndex++] = { | ||
vertices: path, | ||
contour | ||
@@ -76,4 +105,4 @@ }; | ||
} | ||
return {contourSegments, contourPolygons}; | ||
return {lines: contourLines, polygons: contourPolygons}; | ||
} | ||
/* eslint-enable max-depth */ |
@@ -0,1 +1,5 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
// Code to Offsets Map needed to implement Marching Squares algorithm | ||
@@ -2,0 +6,0 @@ // Ref: https://en.wikipedia.org/wiki/Marching_squares |
@@ -0,17 +1,10 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
// All utility methods needed to implement Marching Squares algorithm | ||
// Ref: https://en.wikipedia.org/wiki/Marching_squares | ||
import {log} from '@deck.gl/core'; | ||
import {ISOLINES_CODE_OFFSET_MAP, ISOBANDS_CODE_OFFSET_MAP} from './marching-squares-codes'; | ||
export const CONTOUR_TYPE = { | ||
ISO_LINES: 1, | ||
ISO_BANDS: 2 | ||
}; | ||
const DEFAULT_THRESHOLD_DATA = { | ||
zIndex: 0, | ||
zOffset: 0.005 | ||
}; | ||
// Utility methods | ||
@@ -21,3 +14,5 @@ | ||
// threshold must be a single value or a range (array of size 2) | ||
if (Number.isNaN(weight)) { | ||
return 0; | ||
} | ||
// Iso-bands | ||
@@ -36,3 +31,13 @@ if (Array.isArray(threshold)) { | ||
/* eslint-disable complexity, max-statements*/ | ||
export function getCode(opts) { | ||
export function getCode(opts: { | ||
getValue: (x: number, y: number) => number; | ||
threshold: number | number[]; | ||
x: number; | ||
xRange: [number, number]; | ||
y: number; | ||
yRange: [number, number]; | ||
}): { | ||
code: number; | ||
meanCode: number; | ||
} { | ||
// Assumptions | ||
@@ -42,24 +47,23 @@ // Origin is on bottom-left , and X increase to right, Y to top | ||
// to create a 2X2 cell grid | ||
const {cellWeights, x, y, width, height} = opts; | ||
let threshold = opts.threshold; | ||
if (opts.thresholdValue) { | ||
log.deprecated('thresholdValue', 'threshold')(); | ||
threshold = opts.thresholdValue; | ||
} | ||
const {x, y, xRange, yRange, getValue, threshold} = opts; | ||
const isLeftBoundary = x < 0; | ||
const isRightBoundary = x >= width - 1; | ||
const isBottomBoundary = y < 0; | ||
const isTopBoundary = y >= height - 1; | ||
const isLeftBoundary = x < xRange[0]; | ||
const isRightBoundary = x >= xRange[1] - 1; | ||
const isBottomBoundary = y < yRange[0]; | ||
const isTopBoundary = y >= yRange[1] - 1; | ||
const isBoundary = isLeftBoundary || isRightBoundary || isBottomBoundary || isTopBoundary; | ||
const weights: Record<string, number> = {}; | ||
const codes: Record<string, number> = {}; | ||
let weights: number = 0; | ||
let current: number; | ||
let right: number; | ||
let top: number; | ||
let topRight: number; | ||
// TOP | ||
if (isLeftBoundary || isTopBoundary) { | ||
codes.top = 0; | ||
top = 0; | ||
} else { | ||
weights.top = cellWeights[(y + 1) * width + x]; | ||
codes.top = getVertexCode(weights.top, threshold); | ||
const w = getValue(x, y + 1); | ||
top = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
@@ -69,6 +73,7 @@ | ||
if (isRightBoundary || isTopBoundary) { | ||
codes.topRight = 0; | ||
topRight = 0; | ||
} else { | ||
weights.topRight = cellWeights[(y + 1) * width + x + 1]; | ||
codes.topRight = getVertexCode(weights.topRight, threshold); | ||
const w = getValue(x + 1, y + 1); | ||
topRight = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
@@ -78,6 +83,7 @@ | ||
if (isRightBoundary || isBottomBoundary) { | ||
codes.right = 0; | ||
right = 0; | ||
} else { | ||
weights.right = cellWeights[y * width + x + 1]; | ||
codes.right = getVertexCode(weights.right, threshold); | ||
const w = getValue(x + 1, y); | ||
right = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
@@ -87,9 +93,9 @@ | ||
if (isLeftBoundary || isBottomBoundary) { | ||
codes.current = 0; | ||
current = 0; | ||
} else { | ||
weights.current = cellWeights[y * width + x]; | ||
codes.current = getVertexCode(weights.current, threshold); | ||
const w = getValue(x, y); | ||
current = getVertexCode(w, threshold); | ||
weights += w; | ||
} | ||
const {top, topRight, right, current} = codes; | ||
let code = -1; | ||
@@ -108,6 +114,3 @@ if (Number.isFinite(threshold)) { | ||
if (!isBoundary) { | ||
meanCode = getVertexCode( | ||
(weights.top + weights.topRight + weights.right + weights.current) / 4, | ||
threshold | ||
); | ||
meanCode = getVertexCode(weights / 4, threshold); | ||
} | ||
@@ -120,9 +123,11 @@ return {code, meanCode}; | ||
// [x, y] refers current marching cell, reference vertex is always top-right corner | ||
export function getVertices(opts) { | ||
const {gridOrigin, cellSize, x, y, code, meanCode, type = CONTOUR_TYPE.ISO_LINES} = opts; | ||
const thresholdData = {...DEFAULT_THRESHOLD_DATA, ...opts.thresholdData}; | ||
let offsets = | ||
type === CONTOUR_TYPE.ISO_BANDS | ||
? ISOBANDS_CODE_OFFSET_MAP[code] | ||
: ISOLINES_CODE_OFFSET_MAP[code]; | ||
export function getPolygons(opts: { | ||
x: number; | ||
y: number; | ||
z: number; | ||
code: number; | ||
meanCode: number; | ||
}) { | ||
const {x, y, z, code, meanCode} = opts; | ||
let offsets: any = ISOBANDS_CODE_OFFSET_MAP[code]; | ||
@@ -135,45 +140,51 @@ // handle saddle cases | ||
// Reference vertex is at top-right move to top-right corner | ||
const rX = x + 1; | ||
const rY = y + 1; | ||
const vZ = thresholdData.zIndex * thresholdData.zOffset; | ||
const rX = (x + 1) * cellSize[0]; | ||
const rY = (y + 1) * cellSize[1]; | ||
const refVertexX = gridOrigin[0] + rX; | ||
const refVertexY = gridOrigin[1] + rY; | ||
// offsets format | ||
// ISO_LINES: [[1A, 1B], [2A, 2B]], | ||
// ISO_BANDS: [[1A, 1B, 1C, ...], [2A, 2B, 2C, ...]], | ||
// [[1A, 1B, 1C, ...], [2A, 2B, 2C, ...]], | ||
// vertices format | ||
// [ | ||
// [[x1A, y1A], [x1B, y1B], [x1C, y1C] ... ], | ||
// ... | ||
// ] | ||
// ISO_LINES: [[x1A, y1A], [x1B, y1B], [x2A, x2B], ...], | ||
const polygons: number[][][] = []; | ||
offsets.forEach(polygonOffsets => { | ||
const polygon: number[][] = []; | ||
polygonOffsets.forEach(xyOffset => { | ||
const vX = rX + xyOffset[0]; | ||
const vY = rY + xyOffset[1]; | ||
polygon.push([vX, vY, z]); | ||
}); | ||
polygons.push(polygon); | ||
}); | ||
return polygons; | ||
} | ||
// ISO_BANDS: => confirms to SolidPolygonLayer's simple polygon format | ||
// [ | ||
// [[x1A, y1A], [x1B, y1B], [x1C, y1C] ... ], | ||
// ... | ||
// ] | ||
// Returns intersection vertices for given cellindex | ||
// [x, y] refers current marching cell, reference vertex is always top-right corner | ||
export function getLines(opts: {x: number; y: number; z: number; code: number; meanCode: number}) { | ||
const {x, y, z, code, meanCode} = opts; | ||
let offsets = ISOLINES_CODE_OFFSET_MAP[code]; | ||
if (type === CONTOUR_TYPE.ISO_BANDS) { | ||
const polygons: number[][][] = []; | ||
offsets.forEach(polygonOffsets => { | ||
const polygon: number[][] = []; | ||
polygonOffsets.forEach(xyOffset => { | ||
const vX = refVertexX + xyOffset[0] * cellSize[0]; | ||
const vY = refVertexY + xyOffset[1] * cellSize[1]; | ||
polygon.push([vX, vY, vZ]); | ||
}); | ||
polygons.push(polygon); | ||
}); | ||
return polygons; | ||
// handle saddle cases | ||
if (!Array.isArray(offsets)) { | ||
offsets = offsets[meanCode]; | ||
} | ||
// default case is ISO_LINES | ||
// Reference vertex is at top-right move to top-right corner | ||
const rX = x + 1; | ||
const rY = y + 1; | ||
// offsets format | ||
// [[1A, 1B], [2A, 2B]], | ||
// vertices format | ||
// [[x1A, y1A], [x1B, y1B], [x2A, x2B], ...], | ||
const lines: number[][] = []; | ||
offsets.forEach(xyOffsets => { | ||
xyOffsets.forEach(offset => { | ||
const vX = refVertexX + offset[0] * cellSize[0]; | ||
const vY = refVertexY + offset[1] * cellSize[1]; | ||
lines.push([vX, vY, vZ]); | ||
const vX = rX + offset[0]; | ||
const vY = rY + offset[1]; | ||
lines.push([vX, vY, z]); | ||
}); | ||
@@ -180,0 +191,0 @@ }); |
@@ -0,16 +1,70 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { | ||
CompositeLayer, | ||
log, | ||
Accessor, | ||
Color, | ||
GetPickingInfoParams, | ||
CompositeLayerProps, | ||
createIterable, | ||
Layer, | ||
Material, | ||
project32, | ||
LayersList, | ||
PickingInfo, | ||
Position, | ||
Viewport, | ||
UpdateParameters, | ||
DefaultProps | ||
} from '@deck.gl/core'; | ||
import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator'; | ||
import GPUGridLayer, {GPUGridLayerProps} from '../gpu-grid-layer/gpu-grid-layer'; | ||
import CPUGridLayer, {CPUGridLayerProps} from '../cpu-grid-layer/cpu-grid-layer'; | ||
import {WebGLAggregator, CPUAggregator, AggregationOperation} from '../common/aggregator/index'; | ||
import AggregationLayer from '../common/aggregation-layer'; | ||
import {AggregateAccessor} from '../common/types'; | ||
import {defaultColorRange} from '../common/utils/color-utils'; | ||
import {AttributeWithScale} from '../common/utils/scale-utils'; | ||
import {getBinIdRange} from '../common/utils/bounds-utils'; | ||
import {GridCellLayer} from './grid-cell-layer'; | ||
import {BinOptions, binOptionsUniforms} from './bin-options-uniforms'; | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
function noop() {} | ||
const defaultProps: DefaultProps<GridLayerProps> = { | ||
...GPUGridLayer.defaultProps, | ||
...CPUGridLayer.defaultProps, | ||
gpuAggregation: false | ||
gpuAggregation: false, | ||
// color | ||
colorDomain: null, | ||
colorRange: defaultColorRange, | ||
getColorValue: {type: 'accessor', value: null}, // default value is calculated from `getColorWeight` and `colorAggregation` | ||
getColorWeight: {type: 'accessor', value: 1}, | ||
colorAggregation: 'SUM', | ||
lowerPercentile: {type: 'number', min: 0, max: 100, value: 0}, | ||
upperPercentile: {type: 'number', min: 0, max: 100, value: 100}, | ||
colorScaleType: 'quantize', | ||
onSetColorDomain: noop, | ||
// elevation | ||
elevationDomain: null, | ||
elevationRange: [0, 1000], | ||
getElevationValue: {type: 'accessor', value: null}, // default value is calculated from `getElevationWeight` and `elevationAggregation` | ||
getElevationWeight: {type: 'accessor', value: 1}, | ||
elevationAggregation: 'SUM', | ||
elevationScale: {type: 'number', min: 0, value: 1}, | ||
elevationLowerPercentile: {type: 'number', min: 0, max: 100, value: 0}, | ||
elevationUpperPercentile: {type: 'number', min: 0, max: 100, value: 100}, | ||
elevationScaleType: 'linear', | ||
onSetElevationDomain: noop, | ||
// grid | ||
cellSize: {type: 'number', min: 0, value: 1000}, | ||
coverage: {type: 'number', min: 0, max: 1, value: 1}, | ||
getPosition: {type: 'accessor', value: (x: any) => x.position}, | ||
gridAggregator: {type: 'function', optional: true, value: null}, | ||
extruded: false, | ||
// Optional material for 'lighting' shader module | ||
material: true | ||
}; | ||
@@ -22,19 +76,193 @@ | ||
/** Properties added by GridLayer. */ | ||
type _GridLayerProps<DataT> = CPUGridLayerProps<DataT> & | ||
GPUGridLayerProps<DataT> & { | ||
/** | ||
* Whether the aggregation should be performed in high-precision 64-bit mode. | ||
* @default false | ||
*/ | ||
fp64?: boolean; | ||
type _GridLayerProps<DataT> = { | ||
/** | ||
* Custom accessor to retrieve a grid bin index from each data object. | ||
* Not supported by GPU aggregation. | ||
*/ | ||
gridAggregator?: ((position: number[], cellSize: number) => [number, number]) | null; | ||
/** | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
* @default false | ||
*/ | ||
gpuAggregation?: boolean; | ||
}; | ||
/** | ||
* Size of each cell in meters. | ||
* @default 1000 | ||
*/ | ||
cellSize?: number; | ||
/** | ||
* Color scale domain, default is set to the extent of aggregated weights in each cell. | ||
* @default [min(colorWeight), max(colorWeight)] | ||
*/ | ||
colorDomain?: [number, number] | null; | ||
/** | ||
* Default: [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) `6-class YlOrRd` | ||
*/ | ||
colorRange?: Color[]; | ||
/** | ||
* Cell size multiplier, clamped between 0 - 1. | ||
* @default 1 | ||
*/ | ||
coverage?: number; | ||
/** | ||
* Elevation scale input domain, default is set to between 0 and the max of aggregated weights in each cell. | ||
* @default [0, max(elevationWeight)] | ||
*/ | ||
elevationDomain?: [number, number] | null; | ||
/** | ||
* Elevation scale output range. | ||
* @default [0, 1000] | ||
*/ | ||
elevationRange?: [number, number]; | ||
/** | ||
* Cell elevation multiplier. | ||
* @default 1 | ||
*/ | ||
elevationScale?: number; | ||
/** | ||
* Whether to enable cell elevation. If set to false, all cell will be flat. | ||
* @default true | ||
*/ | ||
extruded?: boolean; | ||
/** | ||
* Filter cells and re-calculate color by `upperPercentile`. | ||
* Cells with value larger than the upperPercentile will be hidden. | ||
* @default 100 | ||
*/ | ||
upperPercentile?: number; | ||
/** | ||
* Filter cells and re-calculate color by `lowerPercentile`. | ||
* Cells with value smaller than the lowerPercentile will be hidden. | ||
* @default 0 | ||
*/ | ||
lowerPercentile?: number; | ||
/** | ||
* Filter cells and re-calculate elevation by `elevationUpperPercentile`. | ||
* Cells with elevation value larger than the `elevationUpperPercentile` will be hidden. | ||
* @default 100 | ||
*/ | ||
elevationUpperPercentile?: number; | ||
/** | ||
* Filter cells and re-calculate elevation by `elevationLowerPercentile`. | ||
* Cells with elevation value larger than the `elevationLowerPercentile` will be hidden. | ||
* @default 0 | ||
*/ | ||
elevationLowerPercentile?: number; | ||
/** | ||
* Scaling function used to determine the color of the grid cell. | ||
* Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'. | ||
* @default 'quantize' | ||
*/ | ||
colorScaleType?: 'quantize' | 'linear' | 'quantile' | 'ordinal'; | ||
/** | ||
* Scaling function used to determine the elevation of the grid cell. | ||
* Supported Values are 'linear' and 'quantile'. | ||
* @default 'linear' | ||
*/ | ||
elevationScaleType?: 'linear' | 'quantile'; | ||
/** | ||
* Material settings for lighting effect. Applies if `extruded: true`. | ||
* | ||
* @default true | ||
* @see https://deck.gl/docs/developer-guide/using-lighting | ||
*/ | ||
material?: Material; | ||
/** | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's color value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
colorAggregation?: AggregationOperation; | ||
/** | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's elevation value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
elevationAggregation?: AggregationOperation; | ||
/** | ||
* Method called to retrieve the position of each object. | ||
* @default object => object.position | ||
*/ | ||
getPosition?: Accessor<DataT, Position>; | ||
/** | ||
* The weight of a data object used to calculate the color value for a cell. | ||
* @default 1 | ||
*/ | ||
getColorWeight?: Accessor<DataT, number>; | ||
/** | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its color is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
*/ | ||
getColorValue?: AggregateAccessor<DataT> | null; | ||
/** | ||
* The weight of a data object used to calculate the elevation value for a cell. | ||
* @default 1 | ||
*/ | ||
getElevationWeight?: Accessor<DataT, number>; | ||
/** | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its elevation is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
*/ | ||
getElevationValue?: AggregateAccessor<DataT> | null; | ||
/** | ||
* This callback will be called when bin color domain has been calculated. | ||
* @default () => {} | ||
*/ | ||
onSetColorDomain?: (minMax: [number, number]) => void; | ||
/** | ||
* This callback will be called when bin elevation domain has been calculated. | ||
* @default () => {} | ||
*/ | ||
onSetElevationDomain?: (minMax: [number, number]) => void; | ||
/** | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
* @default false | ||
*/ | ||
gpuAggregation?: boolean; | ||
}; | ||
export type GridLayerPickingInfo<DataT> = PickingInfo<{ | ||
/** Column index of the picked cell */ | ||
col: number; | ||
/** Row index of the picked cell */ | ||
row: number; | ||
/** Aggregated color value, as determined by `getColorWeight` and `colorAggregation` */ | ||
colorValue: number; | ||
/** Aggregated elevation value, as determined by `getElevationWeight` and `elevationAggregation` */ | ||
elevationValue: number; | ||
/** Number of data points in the picked cell */ | ||
count: number; | ||
/** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */ | ||
pointIndices?: number[]; | ||
/** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */ | ||
points?: DataT[]; | ||
}>; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends CompositeLayer< | ||
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends AggregationLayer< | ||
DataT, | ||
ExtraPropsT & Required<_GridLayerProps<DataT>> | ||
@@ -45,70 +273,383 @@ > { | ||
state!: CompositeLayer['state'] & { | ||
useGPUAggregation: boolean; | ||
}; | ||
state!: AggregationLayer<DataT>['state'] & | ||
BinOptions & { | ||
// Needed if getColorValue, getElevationValue are used | ||
dataAsArray?: DataT[]; | ||
initializeState() { | ||
this.state = { | ||
useGPUAggregation: false // TODO(v9): Re-enable GPU aggregation. | ||
colors?: AttributeWithScale; | ||
elevations?: AttributeWithScale; | ||
binIdRange: [number, number][]; | ||
aggregatorViewport: Viewport; | ||
}; | ||
getAggregatorType(): string { | ||
const {gpuAggregation, gridAggregator, getColorValue, getElevationValue} = this.props; | ||
if (gpuAggregation && (gridAggregator || getColorValue || getElevationValue)) { | ||
// If these features are desired by the app, the user should explicitly use CPU aggregation | ||
log.warn('Features not supported by GPU aggregation, falling back to CPU')(); | ||
return 'cpu'; | ||
} | ||
if ( | ||
// GPU aggregation is requested | ||
gpuAggregation && | ||
// GPU aggregation is supported by the device | ||
WebGLAggregator.isSupported(this.context.device) | ||
) { | ||
return 'gpu'; | ||
} | ||
return 'cpu'; | ||
} | ||
updateState({props}: UpdateParameters<this>) { | ||
this.setState({ | ||
// TODO(v9): Re-enable GPU aggregation. | ||
// useGPUAggregation: this.canUseGPUAggregation(props) | ||
useGPUAggregation: false | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator { | ||
if (type === 'cpu') { | ||
const {gridAggregator, cellSize} = this.props; | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({positions}: {positions: number[]}, index: number, opts: BinOptions) => { | ||
if (gridAggregator) { | ||
return gridAggregator(positions, cellSize); | ||
} | ||
const viewport = this.state.aggregatorViewport; | ||
// project to common space | ||
const p = viewport.projectPosition(positions); | ||
const {cellSizeCommon, cellOriginCommon} = opts; | ||
return [ | ||
Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((p[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}, | ||
getValue: [ | ||
{sources: ['colorWeights'], getValue: ({colorWeights}) => colorWeights}, | ||
{sources: ['elevationWeights'], getValue: ({elevationWeights}) => elevationWeights} | ||
] | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 2, | ||
bufferLayout: this.getAttributeManager()!.getBufferLayouts({isInstanced: false}), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: /* glsl */ ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float colorWeights; | ||
in float elevationWeights; | ||
void getBin(out ivec2 binId) { | ||
vec3 positionCommon = project_position(positions, positions64Low); | ||
vec2 gridCoords = floor(positionCommon.xy / binOptions.cellSizeCommon); | ||
binId = ivec2(gridCoords); | ||
} | ||
void getValue(out vec2 value) { | ||
value = vec2(colorWeights, elevationWeights); | ||
} | ||
` | ||
}) | ||
}); | ||
} | ||
renderLayers(): Layer { | ||
const {data, updateTriggers} = this.props; | ||
const id = this.state.useGPUAggregation ? 'GPU' : 'CPU'; | ||
const LayerType = this.state.useGPUAggregation | ||
? this.getSubLayerClass('GPU', GPUGridLayer) | ||
: this.getSubLayerClass('CPU', CPUGridLayer); | ||
return new LayerType( | ||
this.props, | ||
this.getSubLayerProps({ | ||
id, | ||
updateTriggers | ||
}), | ||
{ | ||
data | ||
initializeState() { | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager()!; | ||
attributeManager.add({ | ||
positions: { | ||
size: 3, | ||
accessor: 'getPosition', | ||
type: 'float64', | ||
fp64: this.use64bitPositions() | ||
}, | ||
colorWeights: {size: 1, accessor: 'getColorWeight'}, | ||
elevationWeights: {size: 1, accessor: 'getElevationWeight'} | ||
}); | ||
} | ||
updateState(params: UpdateParameters<this>) { | ||
const aggregatorChanged = super.updateState(params); | ||
const {props, oldProps, changeFlags} = params; | ||
const {aggregator} = this.state; | ||
if ( | ||
(changeFlags.dataChanged || !this.state.dataAsArray) && | ||
(props.getColorValue || props.getElevationValue) | ||
) { | ||
// Convert data to array | ||
this.state.dataAsArray = Array.from(createIterable(props.data).iterable); | ||
} | ||
if ( | ||
aggregatorChanged || | ||
changeFlags.dataChanged || | ||
props.cellSize !== oldProps.cellSize || | ||
props.getColorValue !== oldProps.getColorValue || | ||
props.getElevationValue !== oldProps.getElevationValue || | ||
props.colorAggregation !== oldProps.colorAggregation || | ||
props.elevationAggregation !== oldProps.elevationAggregation | ||
) { | ||
this._updateBinOptions(); | ||
const {cellSizeCommon, cellOriginCommon, binIdRange, dataAsArray} = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
pointCount: this.getNumInstances(), | ||
operations: [props.colorAggregation, props.elevationAggregation], | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
}, | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
if (dataAsArray) { | ||
const {getColorValue, getElevationValue} = this.props; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by CPUAggregator | ||
customOperations: [ | ||
getColorValue && | ||
((indices: number[]) => | ||
getColorValue( | ||
indices.map(i => dataAsArray[i]), | ||
{indices, data: props.data} | ||
)), | ||
getElevationValue && | ||
((indices: number[]) => | ||
getElevationValue( | ||
indices.map(i => dataAsArray[i]), | ||
{indices, data: props.data} | ||
)) | ||
] | ||
}); | ||
} | ||
); | ||
} | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getColorValue) { | ||
aggregator.setNeedsUpdate(0); | ||
} | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getElevationValue) { | ||
aggregator.setNeedsUpdate(1); | ||
} | ||
return aggregatorChanged; | ||
} | ||
// Private methods | ||
private _updateBinOptions() { | ||
const bounds = this.getBounds(); | ||
const cellSizeCommon: [number, number] = [1, 1]; | ||
let cellOriginCommon: [number, number] = [0, 0]; | ||
let binIdRange: [number, number][] = [ | ||
[0, 1], | ||
[0, 1] | ||
]; | ||
let viewport = this.context.viewport; | ||
canUseGPUAggregation(props: GridLayer['props']) { | ||
const { | ||
gpuAggregation, | ||
lowerPercentile, | ||
upperPercentile, | ||
getColorValue, | ||
getElevationValue, | ||
colorScaleType | ||
} = props; | ||
if (!gpuAggregation) { | ||
// cpu aggregation is requested | ||
return false; | ||
if (bounds && Number.isFinite(bounds[0][0])) { | ||
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]; | ||
const {cellSize} = this.props; | ||
const {unitsPerMeter} = viewport.getDistanceScales(centroid); | ||
cellSizeCommon[0] = unitsPerMeter[0] * cellSize; | ||
cellSizeCommon[1] = unitsPerMeter[1] * cellSize; | ||
// Offset common space to center at the origin of the grid cell where the data center is in | ||
// This improves precision without affecting the cell positions | ||
const centroidCommon = viewport.projectFlat(centroid); | ||
cellOriginCommon = [ | ||
Math.floor(centroidCommon[0] / cellSizeCommon[0]) * cellSizeCommon[0], | ||
Math.floor(centroidCommon[1] / cellSizeCommon[1]) * cellSizeCommon[1] | ||
]; | ||
centroid = viewport.unprojectFlat(cellOriginCommon); | ||
const ViewportType = viewport.constructor as any; | ||
// We construct a viewport for the GPU aggregator's project module | ||
// This viewport is determined by data | ||
// removes arbitrary precision variance that depends on initial view state | ||
viewport = viewport.isGeospatial | ||
? new ViewportType({longitude: centroid[0], latitude: centroid[1], zoom: 12}) | ||
: new Viewport({position: [centroid[0], centroid[1], 0], zoom: 12}); | ||
// Round to the nearest 32-bit float to match CPU and GPU results | ||
cellOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])]; | ||
binIdRange = getBinIdRange({ | ||
dataBounds: bounds, | ||
getBinId: (p: number[]) => { | ||
const positionCommon = viewport.projectFlat(p); | ||
return [ | ||
Math.floor((positionCommon[0] - cellOriginCommon[0]) / cellSizeCommon[0]), | ||
Math.floor((positionCommon[1] - cellOriginCommon[1]) / cellSizeCommon[1]) | ||
]; | ||
} | ||
}); | ||
} | ||
if (!GPUGridAggregator.isSupported(this.context.device)) { | ||
return false; | ||
this.setState({cellSizeCommon, cellOriginCommon, binIdRange, aggregatorViewport: viewport}); | ||
} | ||
override draw(opts) { | ||
// Replaces render time viewport with our own | ||
if (opts.shaderModuleProps.project) { | ||
opts.shaderModuleProps.project.viewport = this.state.aggregatorViewport; | ||
} | ||
if (lowerPercentile !== 0 || upperPercentile !== 100) { | ||
// percentile calculations requires sorting not supported on GPU | ||
return false; | ||
super.draw(opts); | ||
} | ||
private _onAggregationUpdate({channel}: {channel: number}) { | ||
const props = this.getCurrentLayer()!.props; | ||
const {aggregator} = this.state; | ||
if (channel === 0) { | ||
const result = aggregator.getResult(0)!; | ||
this.setState({ | ||
colors: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetColorDomain(aggregator.getResultDomain(0)); | ||
} else if (channel === 1) { | ||
const result = aggregator.getResult(1)!; | ||
this.setState({ | ||
elevations: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetElevationDomain(aggregator.getResultDomain(1)); | ||
} | ||
if (getColorValue !== null || getElevationValue !== null) { | ||
// accessor for custom color or elevation calculation is specified | ||
return false; | ||
} | ||
onAttributeChange(id: string) { | ||
const {aggregator} = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
this._updateBinOptions(); | ||
const {cellSizeCommon, cellOriginCommon, binIdRange} = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
binOptions: { | ||
cellSizeCommon, | ||
cellOriginCommon | ||
} | ||
}); | ||
break; | ||
case 'colorWeights': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
case 'elevationWeights': | ||
aggregator.setNeedsUpdate(1); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
if (colorScaleType === 'quantile' || colorScaleType === 'ordinal') { | ||
// quantile and ordinal scales are not supported on GPU | ||
return false; | ||
} | ||
renderLayers(): LayersList | Layer | null { | ||
const {aggregator, cellOriginCommon, cellSizeCommon} = this.state; | ||
const { | ||
elevationScale, | ||
colorRange, | ||
elevationRange, | ||
extruded, | ||
coverage, | ||
material, | ||
transitions, | ||
colorScaleType, | ||
lowerPercentile, | ||
upperPercentile, | ||
colorDomain, | ||
elevationScaleType, | ||
elevationLowerPercentile, | ||
elevationUpperPercentile, | ||
elevationDomain | ||
} = this.props; | ||
const CellLayerClass = this.getSubLayerClass('cells', GridCellLayer); | ||
const binAttribute = aggregator.getBins(); | ||
const colors = this.state.colors?.update({ | ||
scaleType: colorScaleType, | ||
lowerPercentile, | ||
upperPercentile | ||
}); | ||
const elevations = this.state.elevations?.update({ | ||
scaleType: elevationScaleType, | ||
lowerPercentile: elevationLowerPercentile, | ||
upperPercentile: elevationUpperPercentile | ||
}); | ||
if (!colors || !elevations) { | ||
return null; | ||
} | ||
return true; | ||
return new CellLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'cells' | ||
}), | ||
{ | ||
data: { | ||
length: aggregator.binCount, | ||
attributes: { | ||
getBin: binAttribute, | ||
getColorValue: colors.attribute, | ||
getElevationValue: elevations.attribute | ||
} | ||
}, | ||
// Data has changed shallowly, but we likely don't need to update the attributes | ||
dataComparator: (data, oldData) => data.length === oldData.length, | ||
updateTriggers: { | ||
getBin: [binAttribute], | ||
getColorValue: [colors.attribute], | ||
getElevationValue: [elevations.attribute] | ||
}, | ||
cellOriginCommon, | ||
cellSizeCommon, | ||
elevationScale, | ||
colorRange, | ||
colorScaleType, | ||
elevationRange, | ||
extruded, | ||
coverage, | ||
material, | ||
colorDomain: colors.domain || colorDomain || aggregator.getResultDomain(0), | ||
elevationDomain: elevations.domain || elevationDomain || aggregator.getResultDomain(1), | ||
colorCutoff: colors.cutoff, | ||
elevationCutoff: elevations.cutoff, | ||
transitions: transitions && { | ||
getFillColor: transitions.getColorValue || transitions.getColorWeight, | ||
getElevation: transitions.getElevationValue || transitions.getElevationWeight | ||
}, | ||
// Extensions are already handled by the GPUAggregator, do not pass it down | ||
extensions: [] | ||
} | ||
); | ||
} | ||
getPickingInfo(params: GetPickingInfoParams): GridLayerPickingInfo<DataT> { | ||
const info: GridLayerPickingInfo<DataT> = params.info; | ||
const {index} = info; | ||
if (index >= 0) { | ||
const bin = this.state.aggregator.getBin(index); | ||
let object: GridLayerPickingInfo<DataT>['object']; | ||
if (bin) { | ||
object = { | ||
col: bin.id[0], | ||
row: bin.id[1], | ||
colorValue: bin.value[0], | ||
elevationValue: bin.value[1], | ||
count: bin.count | ||
}; | ||
if (bin.pointIndices) { | ||
object.pointIndices = bin.pointIndices; | ||
object.points = Array.isArray(this.props.data) | ||
? bin.pointIndices.map(i => (this.props.data as DataT[])[i]) | ||
: []; | ||
} | ||
} | ||
info.object = object; | ||
} | ||
return info; | ||
} | ||
} |
@@ -0,1 +1,5 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export function getBounds(points: number[][]): number[] { | ||
@@ -41,3 +45,7 @@ // Now build bounding box in world space (aligned to world coordiante system) | ||
// Expands boundingBox:[xMin, yMin, xMax, yMax] to match aspect ratio of given width and height | ||
export function scaleToAspectRatio(boundingBox: number[], width: number, height: number): number[] { | ||
export function scaleToAspectRatio( | ||
boundingBox: [number, number, number, number], | ||
width: number, | ||
height: number | ||
): [number, number, number, number] { | ||
const [xMin, yMin, xMax, yMax] = boundingBox; | ||
@@ -44,0 +52,0 @@ |
@@ -1,20 +0,4 @@ | ||
// Copyright (c) 2015 - 2019 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
@@ -44,7 +28,8 @@ /* global setTimeout clearTimeout */ | ||
UpdateParameters, | ||
DefaultProps | ||
DefaultProps, | ||
project32 | ||
} from '@deck.gl/core'; | ||
import TriangleLayer from './triangle-layer'; | ||
import AggregationLayer, {AggregationLayerProps} from '../aggregation-layer'; | ||
import {defaultColorRange, colorRangeToFlatArray} from '../utils/color-utils'; | ||
import AggregationLayer, {AggregationLayerProps} from './aggregation-layer'; | ||
import {defaultColorRange, colorRangeToFlatArray} from '../common/utils/color-utils'; | ||
import weightsVs from './weights-vs.glsl'; | ||
@@ -54,2 +39,8 @@ import weightsFs from './weights-fs.glsl'; | ||
import maxFs from './max-fs.glsl'; | ||
import { | ||
MaxWeightProps, | ||
maxWeightUniforms, | ||
WeightProps, | ||
weightUniforms | ||
} from './heatmap-layer-uniforms'; | ||
@@ -206,2 +197,11 @@ const RESOLUTION = 2; // (number of common space pixels) / (number texels) | ||
getShaders(shaders: any) { | ||
let modules = [project32]; | ||
if (shaders.modules) { | ||
modules = [...modules, ...shaders.modules]; | ||
} | ||
return super.getShaders({...shaders, modules}); | ||
} | ||
initializeState() { | ||
@@ -397,3 +397,3 @@ super.initializeAggregationLayer(DIMENSIONS); | ||
_createWeightsTransform(shaders: {vs: string; fs?: string}) { | ||
_createWeightsTransform(shaders: {vs: string; fs?: string; modules: any[]}) { | ||
let {weightsTransform} = this.state; | ||
@@ -418,3 +418,4 @@ const {weightsTexture} = this.state; | ||
topology: 'point-list', | ||
...shaders | ||
...shaders, | ||
modules: [...shaders.modules, weightUniforms] | ||
} as TextureTransformProps); | ||
@@ -436,8 +437,10 @@ | ||
const maxWeightsTransformShaders = this.getShaders({vs: maxVs, fs: maxFs}); | ||
const maxWeightsTransformShaders = this.getShaders({ | ||
vs: maxVs, | ||
fs: maxFs, | ||
modules: [maxWeightUniforms] | ||
}); | ||
const maxWeightTransform = new TextureTransform(device, { | ||
id: `${this.id}-max-weights-transform`, | ||
bindings: {inTexture: weightsTexture}, | ||
uniforms: {textureSize}, | ||
targetTexture: maxWeightsTexture, | ||
targetTexture: maxWeightsTexture!, | ||
...maxWeightsTransformShaders, | ||
@@ -457,2 +460,7 @@ vertexCount: textureSize * textureSize, | ||
const maxWeightProps: MaxWeightProps = {inTexture: weightsTexture!, textureSize}; | ||
maxWeightTransform.model.shaderInputs.setProps({ | ||
maxWeight: maxWeightProps | ||
}); | ||
this.setState({ | ||
@@ -496,4 +504,4 @@ weightsTexture, | ||
viewport.unproject([viewport.width, 0]), | ||
viewport.unproject([viewport.width, viewport.height]), | ||
viewport.unproject([0, viewport.height]) | ||
viewport.unproject([0, viewport.height]), | ||
viewport.unproject([viewport.width, viewport.height]) | ||
].map(p => p.map(Math.fround)); | ||
@@ -561,5 +569,6 @@ | ||
// TODO(v9): Unclear whether `setSubImageData` is a public API, or what to use if not. | ||
(colorTexture as any).setSubImageData({data: colors}); | ||
(colorTexture as any).setTexture2DData({data: colors}); | ||
} else { | ||
colorTexture?.destroy(); | ||
// @ts-expect-error TODO(ib) - texture API change | ||
colorTexture = this.context.device.createTexture({ | ||
@@ -577,3 +586,3 @@ ...TEXTURE_PROPS, | ||
const {radiusPixels, colorDomain, aggregation} = this.props; | ||
const {worldBounds, textureSize, weightsScale} = this.state; | ||
const {worldBounds, textureSize, weightsScale, weightsTexture} = this.state; | ||
const weightsTransform = this.state.weightsTransform!; | ||
@@ -601,7 +610,18 @@ this.state.isWeightMapDirty = false; | ||
const moduleSettings = this.getModuleSettings(); | ||
const uniforms = {radiusPixels, commonBounds, textureWidth: textureSize, weightsScale}; | ||
this._setModelAttributes(weightsTransform.model, attributes); | ||
weightsTransform.model.setVertexCount(this.getNumInstances()); | ||
weightsTransform.model.setUniforms(uniforms); | ||
weightsTransform.model.updateModuleSettings(moduleSettings); | ||
const weightProps: WeightProps = { | ||
radiusPixels, | ||
commonBounds, | ||
textureWidth: textureSize, | ||
weightsScale, | ||
weightsTexture: weightsTexture! | ||
}; | ||
const {viewport, devicePixelRatio, coordinateSystem, coordinateOrigin} = moduleSettings; | ||
const {modelMatrix} = this.props; | ||
weightsTransform.model.shaderInputs.setProps({ | ||
project: {viewport, devicePixelRatio, modelMatrix, coordinateSystem, coordinateOrigin}, | ||
weight: weightProps | ||
}); | ||
weightsTransform.run({ | ||
@@ -637,3 +657,6 @@ parameters: {viewport: [0, 0, textureSize, textureSize]}, | ||
// optput: commonBounds: [minX, minY, maxX, maxY] scaled to fit the current texture | ||
_worldToCommonBounds(worldBounds, opts: {useLayerCoordinateSystem?: boolean} = {}) { | ||
_worldToCommonBounds( | ||
worldBounds, | ||
opts: {useLayerCoordinateSystem?: boolean} = {} | ||
): [number, number, number, number] { | ||
const {useLayerCoordinateSystem = false} = opts; | ||
@@ -640,0 +663,0 @@ const [minLong, minLat, maxLong, maxLat] = worldBounds; |
@@ -0,1 +1,5 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -2,0 +6,0 @@ #version 300 es |
@@ -0,5 +1,8 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
#version 300 es | ||
uniform sampler2D inTexture; | ||
uniform float textureSize; | ||
out vec4 outTexture; | ||
@@ -10,5 +13,5 @@ | ||
// Sample every pixel in texture | ||
int yIndex = gl_VertexID / int(textureSize); | ||
int xIndex = gl_VertexID - (yIndex * int(textureSize)); | ||
vec2 uv = (0.5 + vec2(float(xIndex), float(yIndex))) / textureSize; | ||
int yIndex = gl_VertexID / int(maxWeight.textureSize); | ||
int xIndex = gl_VertexID - (yIndex * int(maxWeight.textureSize)); | ||
vec2 uv = (0.5 + vec2(float(xIndex), float(yIndex))) / maxWeight.textureSize; | ||
outTexture = texture(inTexture, uv); | ||
@@ -15,0 +18,0 @@ |
@@ -1,20 +0,4 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
@@ -27,6 +11,4 @@ export default `\ | ||
uniform float opacity; | ||
uniform sampler2D weightsTexture; | ||
uniform sampler2D colorTexture; | ||
uniform float aggregationMode; | ||
@@ -50,3 +32,3 @@ in vec2 vTexCoords; | ||
if (aggregationMode > 0.5) { | ||
if (triangle.aggregationMode > 0.5) { | ||
weight /= max(1.0, weights.a); | ||
@@ -61,5 +43,5 @@ } | ||
vec4 linearColor = getLinearColor(weight); | ||
linearColor.a *= opacity; | ||
linearColor.a *= layer.opacity; | ||
fragColor = linearColor; | ||
} | ||
`; |
@@ -1,20 +0,4 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
@@ -28,6 +12,2 @@ // Inspired by screen-grid-layer vertex shader in deck.gl | ||
uniform sampler2D maxTexture; | ||
uniform float intensity; | ||
uniform vec2 colorDomain; | ||
uniform float threshold; | ||
uniform float aggregationMode; | ||
@@ -45,12 +25,12 @@ in vec3 positions; | ||
vec4 maxTexture = texture(maxTexture, vec2(0.5)); | ||
float maxValue = aggregationMode < 0.5 ? maxTexture.r : maxTexture.g; | ||
float minValue = maxValue * threshold; | ||
if (colorDomain[1] > 0.) { | ||
float maxValue = triangle.aggregationMode < 0.5 ? maxTexture.r : maxTexture.g; | ||
float minValue = maxValue * triangle.threshold; | ||
if (triangle.colorDomain[1] > 0.) { | ||
// if user specified custom domain use it. | ||
maxValue = colorDomain[1]; | ||
minValue = colorDomain[0]; | ||
maxValue = triangle.colorDomain[1]; | ||
minValue = triangle.colorDomain[0]; | ||
} | ||
vIntensityMax = intensity / maxValue; | ||
vIntensityMin = intensity / minValue; | ||
vIntensityMax = triangle.intensity / maxValue; | ||
vIntensityMin = triangle.intensity / minValue; | ||
} | ||
`; |
@@ -1,20 +0,4 @@ | ||
// Copyright (c) 2015 - 2019 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
@@ -26,7 +10,8 @@ import type {Buffer, Device, Texture} from '@luma.gl/core'; | ||
import fs from './triangle-layer-fragment.glsl'; | ||
import {TriangleProps, triangleUniforms} from './triangle-layer-uniforms'; | ||
type _TriangleLayerProps = { | ||
data: {attributes: {positions: Buffer; texCoords: Buffer}}; | ||
colorDomain: number[]; | ||
aggregationMode: string; | ||
colorDomain: [number, number]; | ||
aggregationMode: number; | ||
threshold: number; | ||
@@ -50,3 +35,3 @@ intensity: number; | ||
getShaders() { | ||
return {vs, fs, modules: [project32]}; | ||
return super.getShaders({vs, fs, modules: [project32, triangleUniforms]}); | ||
} | ||
@@ -59,3 +44,3 @@ | ||
_getModel(device: Device): Model { | ||
const {vertexCount, data, weightsTexture, maxTexture, colorTexture} = this.props; | ||
const {vertexCount, data} = this.props; | ||
@@ -65,3 +50,2 @@ return new Model(device, { | ||
id: this.props.id, | ||
bindings: {weightsTexture, maxTexture, colorTexture}, | ||
attributes: data.attributes, | ||
@@ -72,3 +56,3 @@ bufferLayout: [ | ||
], | ||
topology: 'triangle-fan-webgl', | ||
topology: 'triangle-strip', | ||
vertexCount | ||
@@ -78,14 +62,25 @@ }); | ||
draw({uniforms}): void { | ||
draw(): void { | ||
const {model} = this.state; | ||
const {intensity, threshold, aggregationMode, colorDomain} = this.props; | ||
model.setUniforms({ | ||
...uniforms, | ||
const { | ||
aggregationMode, | ||
colorDomain, | ||
intensity, | ||
threshold, | ||
colorTexture, | ||
maxTexture, | ||
weightsTexture | ||
} = this.props; | ||
const triangleProps: TriangleProps = { | ||
aggregationMode, | ||
colorDomain | ||
}); | ||
colorDomain, | ||
intensity, | ||
threshold, | ||
colorTexture, | ||
maxTexture, | ||
weightsTexture | ||
}; | ||
model.shaderInputs.setProps({triangle: triangleProps}); | ||
model.draw(this.context.renderPass); | ||
} | ||
} |
@@ -0,1 +1,5 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -2,0 +6,0 @@ #version 300 es |
@@ -0,1 +1,5 @@ | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
@@ -7,12 +11,8 @@ #version 300 es | ||
out vec4 weightsTexture; | ||
uniform float radiusPixels; | ||
uniform float textureWidth; | ||
uniform vec4 commonBounds; | ||
uniform float weightsScale; | ||
void main() | ||
{ | ||
weightsTexture = vec4(weights * weightsScale, 0., 0., 1.); | ||
weightsTexture = vec4(weights * weight.weightsScale, 0., 0., 1.); | ||
float radiusTexels = project_pixel_size(radiusPixels) * textureWidth / (commonBounds.z - commonBounds.x); | ||
float radiusTexels = project_pixel_size(weight.radiusPixels) * weight.textureWidth / (weight.commonBounds.z - weight.commonBounds.x); | ||
gl_PointSize = radiusTexels * 2.; | ||
@@ -23,3 +23,3 @@ | ||
// // map xy from commonBounds to [-1, 1] | ||
gl_Position.xy = (commonPosition.xy - commonBounds.xy) / (commonBounds.zw - commonBounds.xy) ; | ||
gl_Position.xy = (commonPosition.xy - weight.commonBounds.xy) / (weight.commonBounds.zw - weight.commonBounds.xy) ; | ||
gl_Position.xy = (gl_Position.xy * 2.) - (1.); | ||
@@ -26,0 +26,0 @@ gl_Position.w = 1.0; |
@@ -1,55 +0,49 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import { | ||
log, | ||
Accessor, | ||
AccessorFunction, | ||
Color, | ||
log, | ||
GetPickingInfoParams, | ||
CompositeLayerProps, | ||
createIterable, | ||
Layer, | ||
Material, | ||
project32, | ||
LayersList, | ||
PickingInfo, | ||
Position, | ||
Material, | ||
Viewport, | ||
UpdateParameters, | ||
DefaultProps | ||
} from '@deck.gl/core'; | ||
import {ColumnLayer} from '@deck.gl/layers'; | ||
import {WebGLAggregator, CPUAggregator, AggregationOperation} from '../common/aggregator/index'; | ||
import AggregationLayer from '../common/aggregation-layer'; | ||
import {AggregateAccessor} from '../common/types'; | ||
import {defaultColorRange} from '../common/utils/color-utils'; | ||
import {AttributeWithScale} from '../common/utils/scale-utils'; | ||
import {getBinIdRange} from '../common/utils/bounds-utils'; | ||
import {defaultColorRange} from '../utils/color-utils'; | ||
import HexagonCellLayer from './hexagon-cell-layer'; | ||
import {pointToHexbin, HexbinVertices, getHexbinCentroid, pointToHexbinGLSL} from './hexbin'; | ||
import {BinOptions, binOptionsUniforms} from './bin-options-uniforms'; | ||
import {pointToHexbin} from './hexagon-aggregator'; | ||
import CPUAggregator from '../utils/cpu-aggregator'; | ||
import AggregationLayer, {AggregationLayerProps} from '../aggregation-layer'; | ||
import {AggregateAccessor} from '../types'; | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
function nop() {} | ||
function noop() {} | ||
const defaultProps: DefaultProps<HexagonLayerProps> = { | ||
gpuAggregation: false, | ||
// color | ||
colorDomain: null, | ||
colorRange: defaultColorRange, | ||
getColorValue: {type: 'accessor', value: null}, // default value is calcuated from `getColorWeight` and `colorAggregation` | ||
getColorValue: {type: 'accessor', value: null}, // default value is calculated from `getColorWeight` and `colorAggregation` | ||
getColorWeight: {type: 'accessor', value: 1}, | ||
colorAggregation: 'SUM', | ||
lowerPercentile: {type: 'number', value: 0, min: 0, max: 100}, | ||
upperPercentile: {type: 'number', value: 100, min: 0, max: 100}, | ||
lowerPercentile: {type: 'number', min: 0, max: 100, value: 0}, | ||
upperPercentile: {type: 'number', min: 0, max: 100, value: 100}, | ||
colorScaleType: 'quantize', | ||
onSetColorDomain: nop, | ||
onSetColorDomain: noop, | ||
@@ -59,29 +53,27 @@ // elevation | ||
elevationRange: [0, 1000], | ||
getElevationValue: {type: 'accessor', value: null}, // default value is calcuated from `getElevationWeight` and `elevationAggregation` | ||
getElevationValue: {type: 'accessor', value: null}, // default value is calculated from `getElevationWeight` and `elevationAggregation` | ||
getElevationWeight: {type: 'accessor', value: 1}, | ||
elevationAggregation: 'SUM', | ||
elevationLowerPercentile: {type: 'number', value: 0, min: 0, max: 100}, | ||
elevationUpperPercentile: {type: 'number', value: 100, min: 0, max: 100}, | ||
elevationScale: {type: 'number', min: 0, value: 1}, | ||
elevationLowerPercentile: {type: 'number', min: 0, max: 100, value: 0}, | ||
elevationUpperPercentile: {type: 'number', min: 0, max: 100, value: 100}, | ||
elevationScaleType: 'linear', | ||
onSetElevationDomain: nop, | ||
onSetElevationDomain: noop, | ||
radius: {type: 'number', value: 1000, min: 1}, | ||
// hexbin | ||
radius: {type: 'number', min: 1, value: 1000}, | ||
coverage: {type: 'number', min: 0, max: 1, value: 1}, | ||
getPosition: {type: 'accessor', value: (x: any) => x.position}, | ||
hexagonAggregator: {type: 'function', optional: true, value: null}, | ||
extruded: false, | ||
hexagonAggregator: pointToHexbin, | ||
getPosition: {type: 'accessor', value: (x: any) => x.position}, | ||
// Optional material for 'lighting' shader module | ||
material: true, | ||
// data filter | ||
_filterData: {type: 'function', value: null, optional: true} | ||
material: true | ||
}; | ||
/** All properties supported by by HexagonLayer. */ | ||
export type HexagonLayerProps<DataT = unknown> = _HexagonLayerProps<DataT> & | ||
AggregationLayerProps<DataT>; | ||
/** All properties supported by HexagonLayer. */ | ||
export type HexagonLayerProps<DataT = unknown> = _HexagonLayerProps<DataT> & CompositeLayerProps; | ||
/** Properties added by HexagonLayer. */ | ||
type _HexagonLayerProps<DataT = unknown> = { | ||
type _HexagonLayerProps<DataT> = { | ||
/** | ||
@@ -94,9 +86,10 @@ * Radius of hexagon bin in meters. The hexagons are pointy-topped (rather than flat-topped). | ||
/** | ||
* Function to aggregate data into hexagonal bins. | ||
* @default d3-hexbin | ||
* Custom accessor to retrieve a hexagonal bin index from each data object. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
*/ | ||
hexagonAggregator?: (props: any, params: any) => any; | ||
hexagonAggregator?: ((position: number[], radius: number) => [number, number]) | null; | ||
/** | ||
* Color scale input domain. | ||
* Color scale domain, default is set to the extent of aggregated weights in each cell. | ||
* @default [min(colorWeight), max(colorWeight)] | ||
@@ -107,4 +100,3 @@ */ | ||
/** | ||
* Specified as an array of colors [color1, color2, ...]. | ||
* @default `6-class YlOrRd` - [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) | ||
* Default: [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) `6-class YlOrRd` | ||
*/ | ||
@@ -114,3 +106,3 @@ colorRange?: Color[]; | ||
/** | ||
* Hexagon radius multiplier, clamped between 0 - 1. | ||
* Cell size multiplier, clamped between 0 - 1. | ||
* @default 1 | ||
@@ -121,3 +113,3 @@ */ | ||
/** | ||
* Elevation scale input domain. The elevation scale is a linear scale that maps number of counts to elevation. | ||
* Elevation scale input domain, default is set to between 0 and the max of aggregated weights in each cell. | ||
* @default [0, max(elevationWeight)] | ||
@@ -134,3 +126,3 @@ */ | ||
/** | ||
* Hexagon elevation multiplier. | ||
* Cell elevation multiplier. | ||
* @default 1 | ||
@@ -142,3 +134,3 @@ */ | ||
* Whether to enable cell elevation. If set to false, all cell will be flat. | ||
* @default false | ||
* @default true | ||
*/ | ||
@@ -148,4 +140,4 @@ extruded?: boolean; | ||
/** | ||
* Filter bins and re-calculate color by `upperPercentile`. | ||
* Hexagons with color value larger than the `upperPercentile` will be hidden. | ||
* Filter cells and re-calculate color by `upperPercentile`. | ||
* Cells with value larger than the upperPercentile will be hidden. | ||
* @default 100 | ||
@@ -156,4 +148,4 @@ */ | ||
/** | ||
* Filter bins and re-calculate color by `lowerPercentile`. | ||
* Hexagons with color value smaller than the `lowerPercentile` will be hidden. | ||
* Filter cells and re-calculate color by `lowerPercentile`. | ||
* Cells with value smaller than the lowerPercentile will be hidden. | ||
* @default 0 | ||
@@ -164,4 +156,4 @@ */ | ||
/** | ||
* Filter bins and re-calculate elevation by `elevationUpperPercentile`. | ||
* Hexagons with elevation value larger than the `elevationUpperPercentile` will be hidden. | ||
* Filter cells and re-calculate elevation by `elevationUpperPercentile`. | ||
* Cells with elevation value larger than the `elevationUpperPercentile` will be hidden. | ||
* @default 100 | ||
@@ -172,4 +164,4 @@ */ | ||
/** | ||
* Filter bins and re-calculate elevation by `elevationLowerPercentile`. | ||
* Hexagons with elevation value larger than the `elevationLowerPercentile` will be hidden. | ||
* Filter cells and re-calculate elevation by `elevationLowerPercentile`. | ||
* Cells with elevation value larger than the `elevationLowerPercentile` will be hidden. | ||
* @default 0 | ||
@@ -181,9 +173,11 @@ */ | ||
* Scaling function used to determine the color of the grid cell, default value is 'quantize'. | ||
* Supported Values are 'quantize', 'quantile' and 'ordinal'. | ||
* Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'. | ||
* @default 'quantize' | ||
*/ | ||
colorScaleType?: 'quantize' | 'quantile' | 'ordinal'; | ||
colorScaleType?: 'quantize' | 'linear' | 'quantile' | 'ordinal'; | ||
/** | ||
* Scaling function used to determine the elevation of the grid cell, only supports 'linear'. | ||
* Supported Values are 'linear' and 'quantile'. | ||
* @default 'linear' | ||
*/ | ||
@@ -202,11 +196,15 @@ elevationScaleType?: 'linear'; | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's color value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
colorAggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
colorAggregation?: AggregationOperation; | ||
/** | ||
* Defines the operation used to aggregate all data object weights to calculate a cell's elevation value. | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
elevationAggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
elevationAggregation?: AggregationOperation; | ||
@@ -217,6 +215,6 @@ /** | ||
*/ | ||
getPosition?: AccessorFunction<DataT, Position>; | ||
getPosition?: Accessor<DataT, Position>; | ||
/** | ||
* The weight of a data object used to calculate the color value for a bin. | ||
* The weight of a data object used to calculate the color value for a cell. | ||
* @default 1 | ||
@@ -227,3 +225,4 @@ */ | ||
/** | ||
* After data objects are aggregated into bins, this accessor is called on each cell to get the value that its color is based on. | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its color is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
@@ -234,3 +233,3 @@ */ | ||
/** | ||
* The weight of a data object used to calculate the elevation value for a bin. | ||
* The weight of a data object used to calculate the elevation value for a cell. | ||
* @default 1 | ||
@@ -241,3 +240,4 @@ */ | ||
/** | ||
* After data objects are aggregated into bins, this accessor is called on each cell to get the value that its elevation is based on. | ||
* After data objects are aggregated into cells, this accessor is called on each cell to get the value that its elevation is based on. | ||
* Not supported by GPU aggregation. | ||
* @default null | ||
@@ -248,3 +248,3 @@ */ | ||
/** | ||
* This callback will be called when cell color domain has been calculated. | ||
* This callback will be called when bin color domain has been calculated. | ||
* @default () => {} | ||
@@ -255,3 +255,3 @@ */ | ||
/** | ||
* This callback will be called when cell elevation domain has been calculated. | ||
* This callback will be called when bin elevation domain has been calculated. | ||
* @default () => {} | ||
@@ -262,152 +262,420 @@ */ | ||
/** | ||
* (Experimental) Filter data objects | ||
* When set to true, aggregation is performed on GPU, provided other conditions are met. | ||
* @default false | ||
*/ | ||
_filterData: null | ((d: DataT) => boolean); | ||
gpuAggregation?: boolean; | ||
}; | ||
/** Aggregates data into a hexagon-based heatmap. The color and height of a hexagon are determined based on the objects it contains. */ | ||
export default class HexagonLayer<DataT, ExtraPropsT extends {} = {}> extends AggregationLayer< | ||
DataT, | ||
ExtraPropsT & Required<_HexagonLayerProps<DataT>> | ||
> { | ||
export type HexagonLayerPickingInfo<DataT> = PickingInfo<{ | ||
/** Column index of the picked cell */ | ||
col: number; | ||
/** Row index of the picked cell */ | ||
row: number; | ||
/** Aggregated color value, as determined by `getColorWeight` and `colorAggregation` */ | ||
colorValue: number; | ||
/** Aggregated elevation value, as determined by `getElevationWeight` and `elevationAggregation` */ | ||
elevationValue: number; | ||
/** Number of data points in the picked cell */ | ||
count: number; | ||
/** Centroid of the hexagon */ | ||
position: [number, number]; | ||
/** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */ | ||
pointIndices?: number[]; | ||
/** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */ | ||
points?: DataT[]; | ||
}>; | ||
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */ | ||
export default class HexagonLayer< | ||
DataT = any, | ||
ExtraPropsT extends {} = {} | ||
> extends AggregationLayer<DataT, ExtraPropsT & Required<_HexagonLayerProps<DataT>>> { | ||
static layerName = 'HexagonLayer'; | ||
static defaultProps = defaultProps; | ||
state!: AggregationLayer<DataT>['state'] & { | ||
cpuAggregator: CPUAggregator; | ||
aggregatorState: CPUAggregator['state']; | ||
vertices: number[][] | null; | ||
}; | ||
initializeState() { | ||
const cpuAggregator = new CPUAggregator({ | ||
getAggregator: props => props.hexagonAggregator, | ||
getCellSize: props => props.radius | ||
state!: AggregationLayer<DataT>['state'] & | ||
BinOptions & { | ||
// Needed if getColorValue, getElevationValue are used | ||
dataAsArray?: DataT[]; | ||
colors?: AttributeWithScale; | ||
elevations?: AttributeWithScale; | ||
binIdRange: [number, number][]; | ||
aggregatorViewport: Viewport; | ||
}; | ||
getAggregatorType(): string { | ||
const {gpuAggregation, hexagonAggregator, getColorValue, getElevationValue} = this.props; | ||
if (gpuAggregation && (hexagonAggregator || getColorValue || getElevationValue)) { | ||
// If these features are desired by the app, the user should explicitly use CPU aggregation | ||
log.warn('Features not supported by GPU aggregation, falling back to CPU')(); | ||
return 'cpu'; | ||
} | ||
if ( | ||
// GPU aggregation is requested | ||
gpuAggregation && | ||
// GPU aggregation is supported by the device | ||
WebGLAggregator.isSupported(this.context.device) | ||
) { | ||
return 'gpu'; | ||
} | ||
return 'cpu'; | ||
} | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator { | ||
if (type === 'cpu') { | ||
const {hexagonAggregator, radius} = this.props; | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({positions}: {positions: number[]}, index: number, opts: BinOptions) => { | ||
if (hexagonAggregator) { | ||
return hexagonAggregator(positions, radius); | ||
} | ||
const viewport = this.state.aggregatorViewport; | ||
// project to common space | ||
const p = viewport.projectPosition(positions); | ||
const {radiusCommon, hexOriginCommon} = opts; | ||
return pointToHexbin( | ||
[p[0] - hexOriginCommon[0], p[1] - hexOriginCommon[1]], | ||
radiusCommon | ||
); | ||
} | ||
}, | ||
getValue: [ | ||
{sources: ['colorWeights'], getValue: ({colorWeights}) => colorWeights}, | ||
{sources: ['elevationWeights'], getValue: ({elevationWeights}) => elevationWeights} | ||
] | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 2, | ||
bufferLayout: this.getAttributeManager()!.getBufferLayouts({isInstanced: false}), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: /* glsl */ ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float colorWeights; | ||
in float elevationWeights; | ||
${pointToHexbinGLSL} | ||
void getBin(out ivec2 binId) { | ||
vec3 positionCommon = project_position(positions, positions64Low); | ||
binId = pointToHexbin(positionCommon.xy, binOptions.radiusCommon); | ||
} | ||
void getValue(out vec2 value) { | ||
value = vec2(colorWeights, elevationWeights); | ||
} | ||
` | ||
}) | ||
}); | ||
} | ||
this.state = { | ||
cpuAggregator, | ||
aggregatorState: cpuAggregator.state, | ||
vertices: null | ||
}; | ||
initializeState() { | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager()!; | ||
attributeManager.add({ | ||
positions: {size: 3, type: 'float64', accessor: 'getPosition'} | ||
positions: { | ||
size: 3, | ||
accessor: 'getPosition', | ||
type: 'float64', | ||
fp64: this.use64bitPositions() | ||
}, | ||
colorWeights: {size: 1, accessor: 'getColorWeight'}, | ||
elevationWeights: {size: 1, accessor: 'getElevationWeight'} | ||
}); | ||
// color and elevation attributes can't be added as attributes | ||
// they are calculated using 'getValue' accessor that takes an array of pints. | ||
} | ||
updateState(opts: UpdateParameters<this>) { | ||
super.updateState(opts); | ||
updateState(params: UpdateParameters<this>) { | ||
const aggregatorChanged = super.updateState(params); | ||
if (opts.changeFlags.propsOrDataChanged) { | ||
const aggregatorState = this.state.cpuAggregator.updateState(opts, { | ||
viewport: this.context.viewport, | ||
attributes: this.getAttributes() | ||
const {props, oldProps, changeFlags} = params; | ||
const {aggregator} = this.state; | ||
if ( | ||
(changeFlags.dataChanged || !this.state.dataAsArray) && | ||
(props.getColorValue || props.getElevationValue) | ||
) { | ||
// Convert data to array | ||
this.state.dataAsArray = Array.from(createIterable(props.data).iterable); | ||
} | ||
if ( | ||
aggregatorChanged || | ||
changeFlags.dataChanged || | ||
props.radius !== oldProps.radius || | ||
props.getColorValue !== oldProps.getColorValue || | ||
props.getElevationValue !== oldProps.getElevationValue || | ||
props.colorAggregation !== oldProps.colorAggregation || | ||
props.elevationAggregation !== oldProps.elevationAggregation | ||
) { | ||
this._updateBinOptions(); | ||
const {radiusCommon, hexOriginCommon, binIdRange, dataAsArray} = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
pointCount: this.getNumInstances(), | ||
operations: [props.colorAggregation, props.elevationAggregation], | ||
binOptions: { | ||
radiusCommon, | ||
hexOriginCommon | ||
}, | ||
onUpdate: this._onAggregationUpdate.bind(this) | ||
}); | ||
if (this.state.aggregatorState.layerData !== aggregatorState.layerData) { | ||
// if user provided custom aggregator and returns hexagonVertices, | ||
// Need to recalculate radius and angle based on vertices | ||
// @ts-expect-error | ||
const {hexagonVertices} = aggregatorState.layerData || {}; | ||
this.setState({ | ||
vertices: hexagonVertices && this.convertLatLngToMeterOffset(hexagonVertices) | ||
if (dataAsArray) { | ||
const {getColorValue, getElevationValue} = this.props; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by CPUAggregator | ||
customOperations: [ | ||
getColorValue && | ||
((indices: number[]) => | ||
getColorValue( | ||
indices.map(i => dataAsArray[i]), | ||
{indices, data: props.data} | ||
)), | ||
getElevationValue && | ||
((indices: number[]) => | ||
getElevationValue( | ||
indices.map(i => dataAsArray[i]), | ||
{indices, data: props.data} | ||
)) | ||
] | ||
}); | ||
} | ||
} | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getColorValue) { | ||
aggregator.setNeedsUpdate(0); | ||
} | ||
if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getElevationValue) { | ||
aggregator.setNeedsUpdate(1); | ||
} | ||
this.setState({ | ||
// make a copy of the internal state of cpuAggregator for testing | ||
aggregatorState | ||
}); | ||
} | ||
return aggregatorChanged; | ||
} | ||
convertLatLngToMeterOffset(hexagonVertices) { | ||
const {viewport} = this.context; | ||
if (Array.isArray(hexagonVertices) && hexagonVertices.length === 6) { | ||
// get centroid of hexagons | ||
const vertex0 = hexagonVertices[0]; | ||
const vertex3 = hexagonVertices[3]; | ||
private _updateBinOptions() { | ||
const bounds = this.getBounds(); | ||
let radiusCommon = 1; | ||
let hexOriginCommon: [number, number] = [0, 0]; | ||
let binIdRange: [number, number][] = [ | ||
[0, 1], | ||
[0, 1] | ||
]; | ||
let viewport = this.context.viewport; | ||
const centroid = [(vertex0[0] + vertex3[0]) / 2, (vertex0[1] + vertex3[1]) / 2]; | ||
const centroidFlat = viewport.projectFlat(centroid); | ||
if (bounds && Number.isFinite(bounds[0][0])) { | ||
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]; | ||
const {radius} = this.props; | ||
const {unitsPerMeter} = viewport.getDistanceScales(centroid); | ||
radiusCommon = unitsPerMeter[0] * radius; | ||
const {metersPerUnit} = viewport.getDistanceScales(centroid); | ||
// Use the centroid of the hex at the center of the data | ||
// This offsets the common space without changing the bins | ||
const centerHex = pointToHexbin(viewport.projectFlat(centroid), radiusCommon); | ||
centroid = viewport.unprojectFlat(getHexbinCentroid(centerHex, radiusCommon)); | ||
// offset all points by centroid to meter offset | ||
const vertices = hexagonVertices.map(vt => { | ||
const vtFlat = viewport.projectFlat(vt); | ||
const ViewportType = viewport.constructor as any; | ||
// We construct a viewport for the GPU aggregator's project module | ||
// This viewport is determined by data | ||
// removes arbitrary precision variance that depends on initial view state | ||
viewport = viewport.isGeospatial | ||
? new ViewportType({longitude: centroid[0], latitude: centroid[1], zoom: 12}) | ||
: new Viewport({position: [centroid[0], centroid[1], 0], zoom: 12}); | ||
return [ | ||
(vtFlat[0] - centroidFlat[0]) * metersPerUnit[0], | ||
(vtFlat[1] - centroidFlat[1]) * metersPerUnit[1] | ||
]; | ||
hexOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])]; | ||
binIdRange = getBinIdRange({ | ||
dataBounds: bounds, | ||
getBinId: (p: number[]) => { | ||
const positionCommon = viewport.projectFlat(p); | ||
positionCommon[0] -= hexOriginCommon[0]; | ||
positionCommon[1] -= hexOriginCommon[1]; | ||
return pointToHexbin(positionCommon, radiusCommon); | ||
}, | ||
padding: 1 | ||
}); | ||
return vertices; | ||
} | ||
log.error('HexagonLayer: hexagonVertices needs to be an array of 6 points')(); | ||
return null; | ||
this.setState({radiusCommon, hexOriginCommon, binIdRange, aggregatorViewport: viewport}); | ||
} | ||
getPickingInfo({info}) { | ||
return this.state.cpuAggregator.getPickingInfo({info}); | ||
override draw(opts) { | ||
// Replaces render time viewport with our own | ||
if (opts.shaderModuleProps.project) { | ||
opts.shaderModuleProps.project.viewport = this.state.aggregatorViewport; | ||
} | ||
super.draw(opts); | ||
} | ||
// create a method for testing | ||
_onGetSublayerColor(cell) { | ||
return this.state.cpuAggregator.getAccessor('fillColor')(cell); | ||
private _onAggregationUpdate({channel}: {channel: number}) { | ||
const props = this.getCurrentLayer()!.props; | ||
const {aggregator} = this.state; | ||
if (channel === 0) { | ||
const result = aggregator.getResult(0)!; | ||
this.setState({ | ||
colors: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetColorDomain(aggregator.getResultDomain(0)); | ||
} else if (channel === 1) { | ||
const result = aggregator.getResult(1)!; | ||
this.setState({ | ||
elevations: new AttributeWithScale(result, aggregator.binCount) | ||
}); | ||
props.onSetElevationDomain(aggregator.getResultDomain(1)); | ||
} | ||
} | ||
// create a method for testing | ||
_onGetSublayerElevation(cell) { | ||
return this.state.cpuAggregator.getAccessor('elevation')(cell); | ||
} | ||
onAttributeChange(id: string) { | ||
const {aggregator} = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
_getSublayerUpdateTriggers() { | ||
return this.state.cpuAggregator.getUpdateTriggers(this.props); | ||
this._updateBinOptions(); | ||
const {radiusCommon, hexOriginCommon, binIdRange} = this.state; | ||
aggregator.setProps({ | ||
// @ts-expect-error only used by GPUAggregator | ||
binIdRange, | ||
binOptions: { | ||
radiusCommon, | ||
hexOriginCommon | ||
} | ||
}); | ||
break; | ||
case 'colorWeights': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
case 'elevationWeights': | ||
aggregator.setNeedsUpdate(1); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
} | ||
renderLayers() { | ||
const {elevationScale, extruded, coverage, material, transitions} = this.props; | ||
const {aggregatorState, vertices} = this.state; | ||
renderLayers(): LayersList | Layer | null { | ||
const {aggregator, radiusCommon, hexOriginCommon} = this.state; | ||
const { | ||
elevationScale, | ||
colorRange, | ||
elevationRange, | ||
extruded, | ||
coverage, | ||
material, | ||
transitions, | ||
colorScaleType, | ||
lowerPercentile, | ||
upperPercentile, | ||
colorDomain, | ||
elevationScaleType, | ||
elevationLowerPercentile, | ||
elevationUpperPercentile, | ||
elevationDomain | ||
} = this.props; | ||
const CellLayerClass = this.getSubLayerClass('cells', HexagonCellLayer); | ||
const binAttribute = aggregator.getBins(); | ||
const SubLayerClass = this.getSubLayerClass('hexagon-cell', ColumnLayer); | ||
const updateTriggers = this._getSublayerUpdateTriggers(); | ||
const colors = this.state.colors?.update({ | ||
scaleType: colorScaleType, | ||
lowerPercentile, | ||
upperPercentile | ||
}); | ||
const elevations = this.state.elevations?.update({ | ||
scaleType: elevationScaleType, | ||
lowerPercentile: elevationLowerPercentile, | ||
upperPercentile: elevationUpperPercentile | ||
}); | ||
const geometry = vertices | ||
? {vertices, radius: 1} | ||
: { | ||
// default geometry | ||
// @ts-expect-error TODO - undefined property? | ||
radius: aggregatorState.layerData.radiusCommon || 1, | ||
radiusUnits: 'common', | ||
angle: 90 | ||
}; | ||
return new SubLayerClass( | ||
if (!colors || !elevations) { | ||
return null; | ||
} | ||
return new CellLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'cells' | ||
}), | ||
{ | ||
...geometry, | ||
data: { | ||
length: aggregator.binCount, | ||
attributes: { | ||
getBin: binAttribute, | ||
getColorValue: colors.attribute, | ||
getElevationValue: elevations.attribute | ||
} | ||
}, | ||
// Data has changed shallowly, but we likely don't need to update the attributes | ||
dataComparator: (data, oldData) => data.length === oldData.length, | ||
updateTriggers: { | ||
getBin: [binAttribute], | ||
getColorValue: [colors.attribute], | ||
getElevationValue: [elevations.attribute] | ||
}, | ||
diskResolution: 6, | ||
vertices: HexbinVertices, | ||
radius: radiusCommon, | ||
hexOriginCommon, | ||
elevationScale, | ||
colorRange, | ||
colorScaleType, | ||
elevationRange, | ||
extruded, | ||
coverage, | ||
material, | ||
getFillColor: this._onGetSublayerColor.bind(this), | ||
getElevation: this._onGetSublayerElevation.bind(this), | ||
colorDomain: colors.domain || colorDomain || aggregator.getResultDomain(0), | ||
elevationDomain: elevations.domain || elevationDomain || aggregator.getResultDomain(1), | ||
colorCutoff: colors.cutoff, | ||
elevationCutoff: elevations.cutoff, | ||
transitions: transitions && { | ||
getFillColor: transitions.getColorValue || transitions.getColorWeight, | ||
getElevation: transitions.getElevationValue || transitions.getElevationWeight | ||
} | ||
}, | ||
this.getSubLayerProps({ | ||
id: 'hexagon-cell', | ||
updateTriggers | ||
}), | ||
{ | ||
data: aggregatorState.layerData.data | ||
}, | ||
// Extensions are already handled by the GPUAggregator, do not pass it down | ||
extensions: [] | ||
} | ||
); | ||
} | ||
getPickingInfo(params: GetPickingInfoParams): HexagonLayerPickingInfo<DataT> { | ||
const info: HexagonLayerPickingInfo<DataT> = params.info; | ||
const {index} = info; | ||
if (index >= 0) { | ||
const bin = this.state.aggregator.getBin(index); | ||
let object: HexagonLayerPickingInfo<DataT>['object']; | ||
if (bin) { | ||
const centroidCommon = getHexbinCentroid( | ||
bin.id as [number, number], | ||
this.state.radiusCommon | ||
); | ||
const centroid = this.context.viewport.unprojectFlat(centroidCommon); | ||
object = { | ||
col: bin.id[0], | ||
row: bin.id[1], | ||
position: centroid, | ||
colorValue: bin.value[0], | ||
elevationValue: bin.value[1], | ||
count: bin.count | ||
}; | ||
if (bin.pointIndices) { | ||
object.pointIndices = bin.pointIndices; | ||
object.points = Array.isArray(this.props.data) | ||
? bin.pointIndices.map(i => (this.props.data as DataT[])[i]) | ||
: []; | ||
} | ||
} | ||
info.object = object; | ||
} | ||
return info; | ||
} | ||
} |
@@ -1,43 +0,30 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export {default as ScreenGridLayer} from './screen-grid-layer/screen-grid-layer'; | ||
export {default as CPUGridLayer} from './cpu-grid-layer/cpu-grid-layer'; | ||
export {default as HexagonLayer} from './hexagon-layer/hexagon-layer'; | ||
export {default as ContourLayer} from './contour-layer/contour-layer'; | ||
export {default as GridLayer} from './grid-layer/grid-layer'; | ||
export {default as GPUGridLayer} from './gpu-grid-layer/gpu-grid-layer'; | ||
export {AGGREGATION_OPERATION} from './utils/aggregation-operation-utils'; | ||
// experimental export | ||
export {default as HeatmapLayer} from './heatmap-layer/heatmap-layer'; | ||
export {default as _GPUGridAggregator} from './utils/gpu-grid-aggregation/gpu-grid-aggregator'; | ||
export {default as _CPUAggregator} from './utils/cpu-aggregator'; | ||
export {default as _AggregationLayer} from './aggregation-layer'; | ||
export {default as _BinSorter} from './utils/bin-sorter'; | ||
export {default as _AggregationLayer} from './common/aggregation-layer'; | ||
export {WebGLAggregator, CPUAggregator} from './common/aggregator/index'; | ||
// types | ||
export type {ContourLayerProps} from './contour-layer/contour-layer'; | ||
export type {ContourLayerProps, ContourLayerPickingInfo} from './contour-layer/contour-layer'; | ||
export type {HeatmapLayerProps} from './heatmap-layer/heatmap-layer'; | ||
export type {HexagonLayerProps} from './hexagon-layer/hexagon-layer'; | ||
export type {CPUGridLayerProps} from './cpu-grid-layer/cpu-grid-layer'; | ||
export type {GridLayerProps} from './grid-layer/grid-layer'; | ||
export type {GPUGridLayerProps} from './gpu-grid-layer/gpu-grid-layer'; | ||
export type {ScreenGridLayerProps} from './screen-grid-layer/screen-grid-layer'; | ||
export type {HexagonLayerProps, HexagonLayerPickingInfo} from './hexagon-layer/hexagon-layer'; | ||
export type {GridLayerProps, GridLayerPickingInfo} from './grid-layer/grid-layer'; | ||
export type { | ||
ScreenGridLayerProps, | ||
ScreenGridLayerPickingInfo | ||
} from './screen-grid-layer/screen-grid-layer'; | ||
export type { | ||
Aggregator, | ||
AggregationOperation, | ||
AggregationProps, | ||
WebGLAggregatorProps, | ||
CPUAggregatorProps | ||
} from './common/aggregator/index'; |
@@ -1,80 +0,53 @@ | ||
// Copyright (c) 2015 - 2019 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import {Texture} from '@luma.gl/core'; | ||
import {Model, Geometry} from '@luma.gl/engine'; | ||
import {Layer, LayerProps, log, picking, UpdateParameters, DefaultProps} from '@deck.gl/core'; | ||
import {defaultColorRange, colorRangeToFlatArray} from '../utils/color-utils'; | ||
import {Layer, picking, UpdateParameters, DefaultProps, Color} from '@deck.gl/core'; | ||
import {createColorRangeTexture, updateColorRangeTexture} from '../common/utils/color-utils'; | ||
import vs from './screen-grid-layer-vertex.glsl'; | ||
import fs from './screen-grid-layer-fragment.glsl'; | ||
import type {_ScreenGridLayerProps} from './screen-grid-layer'; | ||
import {ScreenGridProps, screenGridUniforms} from './screen-grid-layer-uniforms'; | ||
import {ShaderModule} from '@luma.gl/shadertools'; | ||
import type {ScaleType} from '../common/types'; | ||
const DEFAULT_MINCOLOR = [0, 0, 0, 0]; | ||
const DEFAULT_MAXCOLOR = [0, 255, 0, 255]; | ||
const COLOR_PROPS = ['minColor', 'maxColor', 'colorRange', 'colorDomain']; | ||
const defaultProps: DefaultProps<ScreenGridCellLayerProps> = { | ||
cellSizePixels: {type: 'number', value: 100, min: 1}, | ||
cellMarginPixels: {type: 'number', value: 2, min: 0, max: 5}, | ||
colorDomain: null, | ||
colorRange: defaultColorRange | ||
}; | ||
/** All properties supported by ScreenGridCellLayer. */ | ||
export type ScreenGridCellLayerProps<DataT = unknown> = _ScreenGridCellLayerProps<DataT> & | ||
LayerProps; | ||
/** Proprties added by ScreenGridCellLayer. */ | ||
export type _ScreenGridCellLayerProps<DataT> = _ScreenGridLayerProps<DataT> & { | ||
maxTexture: Texture; | ||
export type _ScreenGridCellLayerProps = { | ||
cellSizePixels: number; | ||
cellMarginPixels: number; | ||
colorScaleType: ScaleType; | ||
colorDomain: () => [number, number]; | ||
colorRange?: Color[]; | ||
}; | ||
export default class ScreenGridCellLayer<DataT = any, ExtraPropsT extends {} = {}> extends Layer< | ||
ExtraPropsT & Required<_ScreenGridCellLayerProps<DataT>> | ||
export default class ScreenGridCellLayer<ExtraPropsT extends {} = {}> extends Layer< | ||
ExtraPropsT & Required<_ScreenGridCellLayerProps> | ||
> { | ||
static layerName = 'ScreenGridCellLayer'; | ||
static defaultProps = defaultProps; | ||
state!: { | ||
model?: Model; | ||
colorTexture: Texture; | ||
}; | ||
getShaders(): {vs: string; fs: string; modules: ShaderModule[]} { | ||
return {vs, fs, modules: [picking as ShaderModule]}; | ||
return super.getShaders({vs, fs, modules: [picking, screenGridUniforms]}); | ||
} | ||
initializeState() { | ||
const attributeManager = this.getAttributeManager()!; | ||
attributeManager.addInstanced({ | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
instancePositions: {size: 3, update: this.calculateInstancePositions}, | ||
instanceCounts: {size: 4, noAlloc: true} | ||
this.getAttributeManager()!.addInstanced({ | ||
instancePositions: { | ||
size: 2, | ||
type: 'float32', | ||
accessor: 'getBin' | ||
}, | ||
instanceWeights: { | ||
size: 1, | ||
type: 'float32', | ||
accessor: 'getWeight' | ||
} | ||
}); | ||
this.setState({ | ||
model: this._getModel() | ||
}); | ||
} | ||
shouldUpdateState({changeFlags}) { | ||
// 'instanceCounts' buffer contetns change on viewport change. | ||
return changeFlags.somethingChanged; | ||
this.state.model = this._getModel(); | ||
} | ||
@@ -85,57 +58,48 @@ | ||
const {oldProps, props, changeFlags} = params; | ||
const {props, oldProps, changeFlags} = params; | ||
const model = this.state.model!; | ||
const attributeManager = this.getAttributeManager()!; | ||
if (props.numInstances !== oldProps.numInstances) { | ||
attributeManager.invalidateAll(); | ||
} else if (oldProps.cellSizePixels !== props.cellSizePixels) { | ||
attributeManager.invalidate('instancePositions'); | ||
if (oldProps.colorRange !== props.colorRange) { | ||
this.state.colorTexture?.destroy(); | ||
this.state.colorTexture = createColorRangeTexture( | ||
this.context.device, | ||
props.colorRange, | ||
props.colorScaleType | ||
); | ||
const screenGridProps: Partial<ScreenGridProps> = {colorRange: this.state.colorTexture}; | ||
model.shaderInputs.setProps({screenGrid: screenGridProps}); | ||
} else if (oldProps.colorScaleType !== props.colorScaleType) { | ||
updateColorRangeTexture(this.state.colorTexture, props.colorScaleType); | ||
} | ||
this._updateUniforms(oldProps, props, changeFlags); | ||
if ( | ||
oldProps.cellMarginPixels !== props.cellMarginPixels || | ||
oldProps.cellSizePixels !== props.cellSizePixels || | ||
changeFlags.viewportChanged | ||
) { | ||
const {width, height} = this.context.viewport; | ||
const {cellSizePixels: gridSize, cellMarginPixels} = this.props; | ||
const cellSize = Math.max(gridSize - cellMarginPixels, 0); | ||
const screenGridProps: Partial<ScreenGridProps> = { | ||
gridSizeClipspace: [(gridSize / width) * 2, (gridSize / height) * 2], | ||
cellSizeClipspace: [(cellSize / width) * 2, (cellSize / height) * 2] | ||
}; | ||
model.shaderInputs.setProps({screenGrid: screenGridProps}); | ||
} | ||
} | ||
draw({uniforms}) { | ||
const {parameters, maxTexture} = this.props; | ||
const minColor = this.props.minColor || DEFAULT_MINCOLOR; | ||
const maxColor = this.props.maxColor || DEFAULT_MAXCOLOR; | ||
finalizeState(context) { | ||
super.finalizeState(context); | ||
// If colorDomain not specified we use default domain [1, maxCount] | ||
// maxCount value will be sampled form maxTexture in vertex shader. | ||
const colorDomain = this.props.colorDomain || [1, 0]; | ||
const model = this.state.model!; | ||
model.setUniforms(uniforms); | ||
model.setBindings({ | ||
maxTexture | ||
}); | ||
model.setUniforms({ | ||
// @ts-expect-error stricter luma gl types | ||
minColor, | ||
// @ts-expect-error stricter luma gl types | ||
maxColor, | ||
colorDomain | ||
}); | ||
model.setParameters({ | ||
depthWriteEnabled: false, | ||
// How to specify depth mask in WebGPU? | ||
// depthMask: false, | ||
...parameters | ||
}); | ||
model.draw(this.context.renderPass); | ||
this.state.colorTexture?.destroy(); | ||
} | ||
calculateInstancePositions(attribute, {numInstances}) { | ||
const {width, height} = this.context.viewport; | ||
const {cellSizePixels} = this.props; | ||
const numCol = Math.ceil(width / cellSizePixels); | ||
draw({uniforms}) { | ||
const colorDomain = this.props.colorDomain(); | ||
const model = this.state.model!; | ||
const {value, size} = attribute; | ||
for (let i = 0; i < numInstances; i++) { | ||
const x = i % numCol; | ||
const y = Math.floor(i / numCol); | ||
value[i * size + 0] = ((x * cellSizePixels) / width) * 2 - 1; | ||
value[i * size + 1] = 1 - ((y * cellSizePixels) / height) * 2; | ||
value[i * size + 2] = 0; | ||
} | ||
const screenGridProps: Partial<ScreenGridProps> = {colorDomain}; | ||
model.shaderInputs.setProps({screenGrid: screenGridProps}); | ||
model.draw(this.context.renderPass); | ||
} | ||
@@ -151,13 +115,8 @@ | ||
geometry: new Geometry({ | ||
topology: 'triangle-list', | ||
topology: 'triangle-strip', | ||
attributes: { | ||
// prettier-ignore | ||
positions: new Float32Array([ | ||
0, 0, 0, | ||
1, 0, 0, | ||
1, 1, 0, | ||
0, 0, 0, | ||
1, 1, 0, | ||
0, 1, 0, | ||
]) | ||
positions: { | ||
value: new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), | ||
size: 2 | ||
} | ||
} | ||
@@ -168,46 +127,2 @@ }), | ||
} | ||
_shouldUseMinMax(): boolean { | ||
const {minColor, maxColor, colorDomain, colorRange} = this.props; | ||
if (minColor || maxColor) { | ||
log.deprecated('ScreenGridLayer props: minColor and maxColor', 'colorRange, colorDomain')(); | ||
return true; | ||
} | ||
// minColor and maxColor not supplied, check if colorRange or colorDomain supplied. | ||
// NOTE: colorDomain and colorRange are experimental features, use them only when supplied. | ||
if (colorDomain || colorRange) { | ||
return false; | ||
} | ||
// None specified, use default minColor and maxColor | ||
return true; | ||
} | ||
_updateUniforms(oldProps, props, changeFlags): void { | ||
const model = this.state.model!; | ||
if (COLOR_PROPS.some(key => oldProps[key] !== props[key])) { | ||
model.setUniforms({shouldUseMinMax: this._shouldUseMinMax()}); | ||
} | ||
if (oldProps.colorRange !== props.colorRange) { | ||
model.setUniforms({colorRange: colorRangeToFlatArray(props.colorRange)}); | ||
} | ||
if ( | ||
oldProps.cellMarginPixels !== props.cellMarginPixels || | ||
oldProps.cellSizePixels !== props.cellSizePixels || | ||
changeFlags.viewportChanged | ||
) { | ||
const {width, height} = this.context.viewport; | ||
const {cellSizePixels, cellMarginPixels} = this.props; | ||
const margin = cellSizePixels > cellMarginPixels ? cellMarginPixels : 0; | ||
const cellScale = new Float32Array([ | ||
((cellSizePixels - margin) / width) * 2, | ||
(-(cellSizePixels - margin) / height) * 2, | ||
1 | ||
]); | ||
// @ts-expect-error stricter luma gl types | ||
model.setUniforms({cellScale}); | ||
} | ||
} | ||
} |
@@ -1,23 +0,7 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
/* fragment shader for the grid-layer */ | ||
export default `\ | ||
export default /* glsl */ `\ | ||
#version 300 es | ||
@@ -29,3 +13,2 @@ #define SHADER_NAME screen-grid-layer-fragment-shader | ||
in vec4 vColor; | ||
in float vSampleCount; | ||
@@ -35,5 +18,2 @@ out vec4 fragColor; | ||
void main(void) { | ||
if (vSampleCount <= 0.0) { | ||
discard; | ||
} | ||
fragColor = vColor; | ||
@@ -40,0 +20,0 @@ |
@@ -1,22 +0,6 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
export default `\ | ||
export default /* glsl */ `\ | ||
#version 300 es | ||
@@ -26,61 +10,34 @@ #define SHADER_NAME screen-grid-layer-vertex-shader | ||
in vec3 positions; | ||
in vec3 instancePositions; | ||
in vec4 instanceCounts; | ||
in vec2 positions; | ||
in vec2 instancePositions; | ||
in float instanceWeights; | ||
in vec3 instancePickingColors; | ||
uniform float opacity; | ||
uniform vec3 cellScale; | ||
uniform vec4 minColor; | ||
uniform vec4 maxColor; | ||
uniform vec4 colorRange[RANGE_COUNT]; | ||
uniform vec2 colorDomain; | ||
uniform bool shouldUseMinMax; | ||
uniform sampler2D maxTexture; | ||
uniform sampler2D colorRange; | ||
out vec4 vColor; | ||
out float vSampleCount; | ||
vec4 quantizeScale(vec2 domain, vec4 range[RANGE_COUNT], float value) { | ||
vec4 outColor = vec4(0., 0., 0., 0.); | ||
if (value >= domain.x && value <= domain.y) { | ||
float domainRange = domain.y - domain.x; | ||
if (domainRange <= 0.) { | ||
outColor = colorRange[0]; | ||
} else { | ||
float rangeCount = float(RANGE_COUNT); | ||
float rangeStep = domainRange / rangeCount; | ||
float idx = floor((value - domain.x) / rangeStep); | ||
idx = clamp(idx, 0., rangeCount - 1.); | ||
int intIdx = int(idx); | ||
outColor = colorRange[intIdx]; | ||
} | ||
} | ||
outColor = outColor / 255.; | ||
return outColor; | ||
vec4 interp(float value, vec2 domain, sampler2D range) { | ||
float r = (value - domain.x) / (domain.y - domain.x); | ||
return texture(range, vec2(r, 0.5)); | ||
} | ||
void main(void) { | ||
vSampleCount = instanceCounts.a; | ||
if (isnan(instanceWeights)) { | ||
gl_Position = vec4(0.); | ||
return; | ||
} | ||
float weight = instanceCounts.r; | ||
float maxWeight = texture(maxTexture, vec2(0.5)).r; | ||
vec2 pos = instancePositions * screenGrid.gridSizeClipspace + positions * screenGrid.cellSizeClipspace; | ||
pos.x = pos.x - 1.0; | ||
pos.y = 1.0 - pos.y; | ||
float step = weight / maxWeight; | ||
vec4 minMaxColor = mix(minColor, maxColor, step) / 255.; | ||
gl_Position = vec4(pos, 0., 1.); | ||
vec2 domain = colorDomain; | ||
float domainMaxValid = float(colorDomain.y != 0.); | ||
domain.y = mix(maxWeight, colorDomain.y, domainMaxValid); | ||
vec4 rangeColor = quantizeScale(domain, colorRange, weight); | ||
vColor = interp(instanceWeights, screenGrid.colorDomain, colorRange); | ||
vColor.a *= layer.opacity; | ||
float rangeMinMax = float(shouldUseMinMax); | ||
vec4 color = mix(rangeColor, minMaxColor, rangeMinMax); | ||
vColor = vec4(color.rgb, color.a * opacity); | ||
// Set color to be rendered to picking fbo (also used to check for selection highlight). | ||
picking_setPickingColor(instancePickingColors); | ||
gl_Position = vec4(instancePositions + positions * cellScale, 1.); | ||
} | ||
`; |
@@ -1,20 +0,4 @@ | ||
// 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. | ||
// deck.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
@@ -25,6 +9,6 @@ import { | ||
GetPickingInfoParams, | ||
CompositeLayerProps, | ||
Layer, | ||
LayerContext, | ||
project32, | ||
LayersList, | ||
log, | ||
PickingInfo, | ||
@@ -35,32 +19,23 @@ Position, | ||
} from '@deck.gl/core'; | ||
import type {Buffer, Texture} from '@luma.gl/core'; | ||
import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator'; | ||
import {AGGREGATION_OPERATION, getValueFunc} from '../utils/aggregation-operation-utils'; | ||
import {WebGLAggregator, CPUAggregator, AggregationOperation} from '../common/aggregator/index'; | ||
import AggregationLayer from '../common/aggregation-layer'; | ||
import ScreenGridCellLayer from './screen-grid-cell-layer'; | ||
import GridAggregationLayer, {GridAggregationLayerProps} from '../grid-aggregation-layer'; | ||
import {getFloatTexture} from '../utils/resource-utils'; | ||
import {BinOptions, binOptionsUniforms} from './bin-options-uniforms'; | ||
import {defaultColorRange} from '../common/utils/color-utils'; | ||
const defaultProps: DefaultProps<ScreenGridLayerProps> = { | ||
...(ScreenGridCellLayer.defaultProps as DefaultProps<ScreenGridLayerProps<unknown>>), | ||
cellSizePixels: {type: 'number', value: 100, min: 1}, | ||
cellMarginPixels: {type: 'number', value: 2, min: 0}, | ||
colorRange: defaultColorRange, | ||
colorScaleType: 'linear', | ||
getPosition: {type: 'accessor', value: (d: any) => d.position}, | ||
getWeight: {type: 'accessor', value: 1}, | ||
gpuAggregation: false, // TODO(v9): Re-enable GPU aggregation. | ||
gpuAggregation: false, | ||
aggregation: 'SUM' | ||
}; | ||
const POSITION_ATTRIBUTE_NAME = 'positions'; | ||
const DIMENSIONS = { | ||
data: { | ||
props: ['cellSizePixels'] | ||
}, | ||
weights: { | ||
props: ['aggregation'], | ||
accessors: ['getWeight'] | ||
} | ||
}; | ||
/** All properties supported by ScreenGridLayer. */ | ||
export type ScreenGridLayerProps<DataT = unknown> = _ScreenGridLayerProps<DataT> & | ||
GridAggregationLayerProps<DataT>; | ||
CompositeLayerProps; | ||
@@ -82,16 +57,2 @@ /** Properties added by ScreenGridLayer. */ | ||
/** | ||
* Expressed as an rgba array, minimal color that could be rendered by a tile. | ||
* @default [0, 0, 0, 255] | ||
* @deprecated Deprecated in version 5.2.0, use `colorRange` and `colorDomain` instead. | ||
*/ | ||
minColor?: Color | null; | ||
/** | ||
* Expressed as an rgba array, maximal color that could be rendered by a tile. | ||
* @default [0, 255, 0, 255] | ||
* @deprecated Deprecated in version 5.2.0, use `colorRange` and `colorDomain` instead. | ||
*/ | ||
maxColor?: Color | null; | ||
/** | ||
* Color scale input domain. The color scale maps continues numeric domain into discrete color range. | ||
@@ -110,2 +71,9 @@ * @default [1, max(weight)] | ||
/** | ||
* Scaling function used to determine the color of the grid cell. | ||
* Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'. | ||
* @default 'quantize' | ||
*/ | ||
colorScaleType?: 'linear' | 'quantize'; | ||
/** | ||
* Method called to retrieve the position of each object. | ||
@@ -133,10 +101,24 @@ * | ||
* Defines the type of aggregation operation | ||
* Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'. | ||
* | ||
* V valid values are 'SUM', 'MEAN', 'MIN' and 'MAX'. | ||
* | ||
* @default 'SUM' | ||
*/ | ||
aggregation?: 'SUM' | 'MEAN' | 'MIN' | 'MAX'; | ||
aggregation?: AggregationOperation; | ||
}; | ||
export type ScreenGridLayerPickingInfo<DataT> = PickingInfo<{ | ||
/** Column index of the picked cell, starting from 0 at the left of the viewport */ | ||
col: number; | ||
/** Row index of the picked cell, starting from 0 at the top of the viewport */ | ||
row: number; | ||
/** Aggregated value */ | ||
value: number; | ||
/** Number of data points in the picked cell */ | ||
count: number; | ||
/** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */ | ||
pointIndices?: number[]; | ||
/** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */ | ||
points?: DataT[]; | ||
}>; | ||
/** Aggregates data into histogram bins and renders them as a grid. */ | ||
@@ -146,43 +128,63 @@ export default class ScreenGridLayer< | ||
ExtraProps extends {} = {} | ||
> extends GridAggregationLayer<DataT, ExtraProps & Required<_ScreenGridLayerProps<DataT>>> { | ||
> extends AggregationLayer<DataT, ExtraProps & Required<_ScreenGridLayerProps<DataT>>> { | ||
static layerName = 'ScreenGridLayer'; | ||
static defaultProps = defaultProps; | ||
state!: GridAggregationLayer<DataT>['state'] & { | ||
supported: boolean; | ||
gpuGridAggregator?: any; | ||
gpuAggregation?: any; | ||
weights?: any; | ||
maxTexture?: Texture; | ||
aggregationBuffer?: Buffer; | ||
maxBuffer?: Buffer; | ||
}; | ||
getAggregatorType(): string { | ||
return this.props.gpuAggregation && WebGLAggregator.isSupported(this.context.device) | ||
? 'gpu' | ||
: 'cpu'; | ||
} | ||
createAggregator(type: string): WebGLAggregator | CPUAggregator { | ||
if (type === 'cpu' || !WebGLAggregator.isSupported(this.context.device)) { | ||
return new CPUAggregator({ | ||
dimensions: 2, | ||
getBin: { | ||
sources: ['positions'], | ||
getValue: ({positions}: {positions: number[]}, index: number, opts: BinOptions) => { | ||
const viewport = this.context.viewport; | ||
const p = viewport.project(positions); | ||
const cellSizePixels: number = opts.cellSizePixels; | ||
if (p[0] < 0 || p[0] >= viewport.width || p[1] < 0 || p[1] >= viewport.height) { | ||
// Not on screen | ||
return null; | ||
} | ||
return [Math.floor(p[0] / cellSizePixels), Math.floor(p[1] / cellSizePixels)]; | ||
} | ||
}, | ||
getValue: [{sources: ['counts'], getValue: ({counts}) => counts}] | ||
}); | ||
} | ||
return new WebGLAggregator(this.context.device, { | ||
dimensions: 2, | ||
channelCount: 1, | ||
bufferLayout: this.getAttributeManager()!.getBufferLayouts({isInstanced: false}), | ||
...super.getShaders({ | ||
modules: [project32, binOptionsUniforms], | ||
vs: ` | ||
in vec3 positions; | ||
in vec3 positions64Low; | ||
in float counts; | ||
void getBin(out ivec2 binId) { | ||
vec4 pos = project_position_to_clipspace(positions, positions64Low, vec3(0.0)); | ||
vec2 screenCoords = vec2(pos.x / pos.w + 1.0, 1.0 - pos.y / pos.w) / 2.0 * project.viewportSize / project.devicePixelRatio; | ||
vec2 gridCoords = floor(screenCoords / binOptions.cellSizePixels); | ||
binId = ivec2(gridCoords); | ||
} | ||
void getValue(out float weight) { | ||
weight = counts; | ||
} | ||
` | ||
}) | ||
}); | ||
} | ||
initializeState() { | ||
super.initializeAggregationLayer({ | ||
dimensions: DIMENSIONS, | ||
// @ts-expect-error | ||
getCellSize: props => props.cellSizePixels // TODO | ||
}); | ||
const weights = { | ||
count: { | ||
size: 1, | ||
operation: AGGREGATION_OPERATION.SUM, | ||
needMax: true, | ||
maxTexture: getFloatTexture(this.context.device, {id: `${this.id}-max-texture`}) | ||
} | ||
}; | ||
this.setState({ | ||
supported: true, | ||
projectPoints: true, // aggregation in screen space | ||
weights, | ||
subLayerData: {attributes: {}}, | ||
maxTexture: weights.count.maxTexture, | ||
positionAttributeName: 'positions', | ||
posOffset: [0, 0], | ||
translation: [1, -1] | ||
}); | ||
super.initializeState(); | ||
const attributeManager = this.getAttributeManager()!; | ||
attributeManager.add({ | ||
[POSITION_ATTRIBUTE_NAME]: { | ||
positions: { | ||
size: 3, | ||
@@ -194,3 +196,3 @@ accessor: 'getPosition', | ||
// this attribute is used in gpu aggregation path only | ||
count: {size: 3, accessor: 'getWeight'} | ||
counts: {size: 1, accessor: 'getWeight'} | ||
}); | ||
@@ -200,17 +202,67 @@ } | ||
shouldUpdateState({changeFlags}: UpdateParameters<this>) { | ||
return this.state.supported && changeFlags.somethingChanged; | ||
return changeFlags.somethingChanged; | ||
} | ||
updateState(opts: UpdateParameters<this>) { | ||
super.updateState(opts); | ||
updateState(params: UpdateParameters<this>) { | ||
const aggregatorChanged = super.updateState(params); | ||
const {props, oldProps, changeFlags} = params; | ||
const {cellSizePixels, aggregation} = props; | ||
if ( | ||
aggregatorChanged || | ||
changeFlags.dataChanged || | ||
changeFlags.updateTriggersChanged || | ||
changeFlags.viewportChanged || | ||
aggregation !== oldProps.aggregation || | ||
cellSizePixels !== oldProps.cellSizePixels | ||
) { | ||
const {width, height} = this.context.viewport; | ||
const {aggregator} = this.state; | ||
if (aggregator instanceof WebGLAggregator) { | ||
aggregator.setProps({ | ||
binIdRange: [ | ||
[0, Math.ceil(width / cellSizePixels)], | ||
[0, Math.ceil(height / cellSizePixels)] | ||
] | ||
}); | ||
} | ||
aggregator.setProps({ | ||
pointCount: this.getNumInstances(), | ||
operations: [aggregation], | ||
binOptions: { | ||
cellSizePixels | ||
} | ||
}); | ||
} | ||
if (changeFlags.viewportChanged) { | ||
// Rerun aggregation on viewport change | ||
this.state.aggregator.setNeedsUpdate(); | ||
} | ||
return aggregatorChanged; | ||
} | ||
renderLayers(): LayersList | Layer { | ||
if (!this.state.supported) { | ||
return []; | ||
onAttributeChange(id: string) { | ||
const {aggregator} = this.state; | ||
switch (id) { | ||
case 'positions': | ||
aggregator.setNeedsUpdate(); | ||
break; | ||
case 'counts': | ||
aggregator.setNeedsUpdate(0); | ||
break; | ||
default: | ||
// This should not happen | ||
} | ||
const {maxTexture, numRow, numCol, weights} = this.state; | ||
const {updateTriggers} = this.props; | ||
const {aggregationBuffer} = weights.count; | ||
} | ||
renderLayers(): LayersList | Layer | null { | ||
const {aggregator} = this.state; | ||
const CellLayerClass = this.getSubLayerClass('cells', ScreenGridCellLayer); | ||
const binAttribute = aggregator.getBins(); | ||
const weightAttribute = aggregator.getResult(0); | ||
@@ -220,9 +272,26 @@ return new CellLayerClass( | ||
this.getSubLayerProps({ | ||
id: 'cell-layer', | ||
updateTriggers | ||
id: 'cell-layer' | ||
}), | ||
{ | ||
data: {attributes: {instanceCounts: aggregationBuffer}}, | ||
maxTexture, | ||
numInstances: numRow * numCol | ||
data: { | ||
length: aggregator.binCount, | ||
attributes: { | ||
getBin: binAttribute, | ||
getWeight: weightAttribute | ||
} | ||
}, | ||
// Data has changed shallowly, but we likely don't need to update the attributes | ||
dataComparator: (data, oldData) => data.length === oldData.length, | ||
updateTriggers: { | ||
getBin: [binAttribute], | ||
getWeight: [weightAttribute] | ||
}, | ||
parameters: { | ||
depthWriteEnabled: false, | ||
...this.props.parameters | ||
}, | ||
// Evaluate domain at draw() time | ||
colorDomain: () => this.props.colorDomain || aggregator.getResultDomain(0), | ||
// Extensions are already handled by the GPUAggregator, do not pass it down | ||
extensions: [] | ||
} | ||
@@ -232,27 +301,23 @@ ); | ||
finalizeState(context: LayerContext): void { | ||
super.finalizeState(context); | ||
const {aggregationBuffer, maxBuffer, maxTexture} = this.state; | ||
aggregationBuffer?.delete(); | ||
maxBuffer?.delete(); | ||
maxTexture?.delete(); | ||
} | ||
getPickingInfo({info}: GetPickingInfoParams): PickingInfo { | ||
getPickingInfo(params: GetPickingInfoParams): ScreenGridLayerPickingInfo<DataT> { | ||
const info: ScreenGridLayerPickingInfo<DataT> = params.info; | ||
const {index} = info; | ||
if (index >= 0) { | ||
const {gpuGridAggregator, gpuAggregation, weights} = this.state; | ||
// Get count aggregation results | ||
const aggregationResults = gpuAggregation | ||
? gpuGridAggregator.getData('count') | ||
: weights.count; | ||
// Each instance (one cell) is aggregated into single pixel, | ||
// Get current instance's aggregation details. | ||
info.object = GPUGridAggregator.getAggregationData({ | ||
pixelIndex: index, | ||
...aggregationResults | ||
}); | ||
const bin = this.state.aggregator.getBin(index); | ||
let object: ScreenGridLayerPickingInfo<DataT>['object']; | ||
if (bin) { | ||
object = { | ||
col: bin.id[0], | ||
row: bin.id[1], | ||
value: bin.value[0], | ||
count: bin.count | ||
}; | ||
if (bin.pointIndices) { | ||
object.pointIndices = bin.pointIndices; | ||
object.points = Array.isArray(this.props.data) | ||
? bin.pointIndices.map(i => (this.props.data as DataT[])[i]) | ||
: []; | ||
} | ||
} | ||
info.object = object; | ||
} | ||
@@ -262,95 +327,2 @@ | ||
} | ||
// Aggregation Overrides | ||
updateResults({aggregationData, maxData}) { | ||
const {count} = this.state.weights; | ||
count.aggregationData = aggregationData; | ||
count.aggregationBuffer.write(aggregationData); | ||
count.maxData = maxData; | ||
count.maxTexture.setImageData({data: maxData}); | ||
} | ||
/* eslint-disable complexity, max-statements */ | ||
updateAggregationState(opts) { | ||
const cellSize = opts.props.cellSizePixels; | ||
const cellSizeChanged = opts.oldProps.cellSizePixels !== cellSize; | ||
const {viewportChanged} = opts.changeFlags; | ||
let gpuAggregation = opts.props.gpuAggregation; | ||
if (this.state.gpuAggregation !== opts.props.gpuAggregation) { | ||
if (gpuAggregation && !GPUGridAggregator.isSupported(this.context.device)) { | ||
log.warn('GPU Grid Aggregation not supported, falling back to CPU')(); | ||
gpuAggregation = false; | ||
} | ||
} | ||
const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; | ||
this.setState({ | ||
gpuAggregation | ||
}); | ||
const positionsChanged = this.isAttributeChanged(POSITION_ATTRIBUTE_NAME); | ||
const {dimensions} = this.state; | ||
const {data, weights} = dimensions; | ||
const aggregationDataDirty = | ||
positionsChanged || | ||
gpuAggregationChanged || | ||
viewportChanged || | ||
this.isAggregationDirty(opts, { | ||
compareAll: gpuAggregation, // check for all (including extentions props) when using gpu aggregation | ||
dimension: data | ||
}); | ||
const aggregationWeightsDirty = this.isAggregationDirty(opts, {dimension: weights}); | ||
this.setState({ | ||
aggregationDataDirty, | ||
aggregationWeightsDirty | ||
}); | ||
const {viewport} = this.context; | ||
if (viewportChanged || cellSizeChanged) { | ||
const {width, height} = viewport; | ||
const numCol = Math.ceil(width / cellSize); | ||
const numRow = Math.ceil(height / cellSize); | ||
this.allocateResources(numRow, numCol); | ||
this.setState({ | ||
// transformation from clipspace to screen(pixel) space | ||
scaling: [width / 2, -height / 2, 1], | ||
gridOffset: {xOffset: cellSize, yOffset: cellSize}, | ||
width, | ||
height, | ||
numCol, | ||
numRow | ||
}); | ||
} | ||
if (aggregationWeightsDirty) { | ||
this._updateAccessors(opts); | ||
} | ||
if (aggregationDataDirty || aggregationWeightsDirty) { | ||
this._resetResults(); | ||
} | ||
} | ||
/* eslint-enable complexity, max-statements */ | ||
// Private | ||
_updateAccessors(opts) { | ||
const {getWeight, aggregation, data} = opts.props; | ||
const {count} = this.state.weights; | ||
if (count) { | ||
count.getWeight = getWeight; | ||
count.operation = AGGREGATION_OPERATION[aggregation]; | ||
} | ||
this.setState({getValue: getValueFunc(aggregation, getWeight, {data})}); | ||
} | ||
_resetResults() { | ||
const {count} = this.state.weights; | ||
if (count) { | ||
count.aggregationData = null; | ||
} | ||
} | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
222
1206836
9
23615
2
+ Added@math.gl/core@^4.1.0
- Removed@deck.gl/core@9.1.0(transitive)
- Removed@deck.gl/layers@9.1.0(transitive)
- Removed@loaders.gl/core@4.3.3(transitive)
- Removed@loaders.gl/images@4.3.3(transitive)
- Removed@loaders.gl/loader-utils@4.3.3(transitive)
- Removed@loaders.gl/schema@4.3.3(transitive)
- Removed@loaders.gl/worker-utils@4.3.3(transitive)
- Removed@luma.gl/constants@9.0.28(transitive)
- Removed@luma.gl/core@9.0.28(transitive)
- Removed@luma.gl/engine@9.0.28(transitive)
- Removed@luma.gl/shadertools@9.0.28(transitive)
- Removed@luma.gl/webgl@9.1.0(transitive)
- Removed@mapbox/tiny-sdf@2.0.6(transitive)
- Removed@math.gl/polygon@4.1.0(transitive)
- Removed@math.gl/sun@4.1.0(transitive)
- Removed@types/geojson@7946.0.16(transitive)
- Removedearcut@2.2.4(transitive)
- Removedgl-matrix@3.4.3(transitive)
- Removedmjolnir.js@3.0.0(transitive)
Updated@math.gl/web-mercator@^4.1.0