New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@deck.gl/aggregation-layers

Package Overview
Dependencies
Maintainers
5
Versions
408
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@deck.gl/aggregation-layers - npm Package Compare versions

Comparing version 9.0.35 to 9.1.0-beta.1

dist/common/aggregation-layer.d.ts

92

dist/contour-layer/contour-layer.d.ts

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc