Skip to content

Commit

Permalink
CARTO: Implement QuadbinHeatmapTileLayer (#8703)
Browse files Browse the repository at this point in the history
  • Loading branch information
felixpalmer committed Apr 29, 2024
1 parent 1e53513 commit a287eba
Show file tree
Hide file tree
Showing 5 changed files with 504 additions and 2 deletions.
5 changes: 3 additions & 2 deletions modules/carto/package.json
Expand Up @@ -48,6 +48,8 @@
"@loaders.gl/schema": "^4.2.0",
"@loaders.gl/tiles": "^4.2.0",
"@luma.gl/constants": "^9.0.11",
"@luma.gl/core": "^9.0.11",
"@luma.gl/shadertools": "^9.0.11",
"@math.gl/web-mercator": "^4.0.0",
"@types/d3-array": "^3.0.2",
"@types/d3-color": "^1.4.2",
Expand All @@ -69,8 +71,7 @@
"@deck.gl/extensions": "^9.0.0-beta",
"@deck.gl/geo-layers": "^9.0.0-beta",
"@deck.gl/layers": "^9.0.0-beta",
"@loaders.gl/core": "^4.2.0",
"@luma.gl/core": "^9.0.0"
"@loaders.gl/core": "^4.2.0"
},
"gitHead": "13ace64fc2cee08c133afc882fc307253489a4e4"
}
3 changes: 3 additions & 0 deletions modules/carto/src/index.ts
@@ -1,12 +1,14 @@
import {default as H3TileLayer} from './layers/h3-tile-layer';
import {default as _PointLabelLayer} from './layers/point-label-layer';
import {default as QuadbinTileLayer} from './layers/quadbin-tile-layer';
import {default as QuadbinHeatmapTileLayer} from './layers/quadbin-heatmap-tile-layer';
import {default as RasterTileLayer} from './layers/raster-tile-layer';
import {default as VectorTileLayer} from './layers/vector-tile-layer';
const CARTO_LAYERS = {
H3TileLayer,
_PointLabelLayer,
QuadbinTileLayer,
QuadbinHeatmapTileLayer,
RasterTileLayer,
VectorTileLayer
};
Expand All @@ -15,6 +17,7 @@ export {
H3TileLayer,
_PointLabelLayer,
QuadbinTileLayer,
QuadbinHeatmapTileLayer,
RasterTileLayer,
VectorTileLayer
};
Expand Down
196 changes: 196 additions & 0 deletions modules/carto/src/layers/heatmap.ts
@@ -0,0 +1,196 @@
import type {ShaderPass} from '@luma.gl/shadertools';
import {random} from '@luma.gl/shadertools';
import {Color} from '@deck.gl/core';
const glsl = (s: TemplateStringsArray) => `${s}`;

/**
* @filter Heatmap
* @param radiusPixels Blur radius in pixels, controls smoothness of heatmap
* @param colorDomain Domain to apply to values prior to applying color scale
* @param color1-6 Colors to use in color scale
*/

const fs = glsl`\
uniform heatmapUniforms {
float radiusPixels;
vec2 colorDomain;
vec3 color1;
vec3 color2;
vec3 color3;
vec3 color4;
vec3 color5;
vec3 color6;
float opacity;
} heatmap;
// Controls quality of heatmap, larger values increase quality at expense of performance
const float SUPPORT = 8.0;
const vec4 STOPS = vec4(0.2, 0.4, 0.6, 0.8);
vec3 colorGradient(float value) {
vec3 c1;
vec3 c2;
vec2 range;
if (value < STOPS.x) {
range = vec2(0.0, STOPS.x);
c1 = heatmap.color1; c2 = heatmap.color2;
} else if (value < STOPS.y) {
range = STOPS.xy;
c1 = heatmap.color2; c2 = heatmap.color3;
} else if (value < STOPS.z) {
range = STOPS.yz;
c1 = heatmap.color3; c2 = heatmap.color4;
} else if (value < STOPS.w) {
range = STOPS.zw;
c1 = heatmap.color4; c2 = heatmap.color5;
} else if (value < 1.0 ) {
range = vec2(STOPS.w, 1.0);
c1 = heatmap.color5; c2 = heatmap.color6;
} else {
// Fade out to white
range = vec2(1.0, 10.0);
c1 = heatmap.color6; c2 = vec3(255.0);
}
float f = (value - range.x) / (range.y - range.x);
return mix(c1, c2, f) / 255.0;
}
const vec3 SHIFT = vec3(1.0, 256.0, 256.0 * 256.0);
float unpack(vec3 color) {
return 255.0 * dot(color, SHIFT);
}
vec4 heatmap_sampleColor(sampler2D source, vec2 texSize, vec2 texCoord) {
float accumulator = 0.0;
// Randomize the lookup values to hide the fixed number of samples
float offset = 0.5 * random(vec3(12.9898, 78.233, 151.7182), 0.0);
// Gaussian normalization parameters
const float sigma = SUPPORT / 3.0;
const float a = -0.5 / (sigma * sigma);
const float w0 = 1.0 / (2.0 * 3.141592653589793 * sigma * sigma); // 2D normalization
for (float t = -SUPPORT; t <= SUPPORT; t++) {
for (float s = -SUPPORT; s <= SUPPORT; s++) {
vec2 percent = (vec2(s, t) + offset - 0.5) / SUPPORT;
vec2 delta = percent * heatmap.radiusPixels / texSize;
vec4 offsetColor = texture(source, texCoord + delta);
// Unpack float
float value = unpack(offsetColor.rgb);
// Gaussian
float weight = w0 * exp(a * (s * s + t * t));
accumulator += value * weight;
}
}
// Apply domain
float f = (accumulator - heatmap.colorDomain[0]) / (heatmap.colorDomain[1] - heatmap.colorDomain[0]);
// Color map
vec4 color = vec4(0.0);
color.rgb = colorGradient(f);
color.a = smoothstep(0.0, 0.1, f);
color.a = pow(color.a, 1.0 / 2.2);
color.a *= heatmap.opacity;
return color;
}
`;

const defaultColorRange: Color[] = [
[255, 255, 178],
[254, 217, 118],
[254, 178, 76],
[253, 141, 60],
[240, 59, 32],
[189, 0, 38]
];

export type HeatmapProps = {
/**
* Radius of the circle in pixels, to which the weight of an object is distributed.
*
* @default 30
*/
radiusPixels?: number;
/**
* Controls how weight values are mapped to the `colorRange`, as an array of two numbers [`minValue`, `maxValue`].
*
* @default [0, 1]
*/
colorDomain?: [number, number];
/**
* Specified as an array of colors [color1, color2, ...].
*
* @default `6-class YlOrRd` - [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6)
*/
colorRange: Color[];
opacity: number;
};

export type HeatmapUniforms = {
radiusPixels?: number;
colorDomain?: [number, number];
color1?: [number, number, number];
color2?: [number, number, number];
color3?: [number, number, number];
color4?: [number, number, number];
color5?: [number, number, number];
color6?: [number, number, number];
opacity?: number;
};

export const heatmap: ShaderPass<HeatmapProps, HeatmapUniforms> = {
name: 'heatmap',
uniformPropTypes: {
radiusPixels: {value: 20, min: 0, softMax: 100},
colorDomain: {value: [0, 1]},
color1: {value: [0, 0, 0]},
color2: {value: [0, 0, 0]},
color3: {value: [0, 0, 0]},
color4: {value: [0, 0, 0]},
color5: {value: [0, 0, 0]},
color6: {value: [0, 0, 0]},
opacity: {value: 1, min: 0, max: 1}
},
uniformTypes: {
radiusPixels: 'f32',
colorDomain: 'vec2<f32>',
color1: 'vec3<f32>',
color2: 'vec3<f32>',
color3: 'vec3<f32>',
color4: 'vec3<f32>',
color5: 'vec3<f32>',
color6: 'vec3<f32>',
opacity: 'f32'
},
getUniforms: opts => {
const {
colorRange = defaultColorRange,
radiusPixels = 20,
colorDomain = [0, 1],
opacity = 1
} = opts as HeatmapProps;
const [color1, color2, color3, color4, color5, color6] = colorRange;
return {
color1,
color2,
color3,
color4,
color5,
color6,
radiusPixels,
colorDomain,
opacity
};
},
dependencies: [random],
fs,
passes: [{sampler: true}]
};

0 comments on commit a287eba

Please sign in to comment.