@orca-fe/x-map
Version:
249 lines (248 loc) • 10.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const EffectComposer_1 = require("three/examples/jsm/postprocessing/EffectComposer");
const RenderPass_1 = require("three/examples/jsm/postprocessing/RenderPass");
const ShaderPass_1 = require("three/examples/jsm/postprocessing/ShaderPass");
const three_1 = require("three");
const ThreeLayer_1 = tslib_1.__importDefault(require("./ThreeLayer"));
const coord_1 = require("../utils/coord");
const defaultColorsDefine = [
'rgba(0,0,0,0)',
'rgba(17,21,245,0.1)',
'rgba(17,158,245,0.6)',
'rgba(99,245,17,0.5)',
'rgba(252,229,9,0.8)',
'rgba(246,150,11,0.8)',
'rgba(255,68,68,0.8)',
];
const defaultColors = defaultColorsDefine.map((color, index) => [
index / (defaultColorsDefine.length - 1),
color,
]);
class ThreeHeatmapLayer extends ThreeLayer_1.default {
constructor(options = {}) {
var _a;
const { colors = defaultColors, colorPrecision, data, min = 0, dataPrecision = 20, max, z = 1, opacity = 0.9, maxZoom, minZoom, radius = 30 } = options, otherOptions = tslib_1.__rest(options, ["colors", "colorPrecision", "data", "min", "dataPrecision", "max", "z", "opacity", "maxZoom", "minZoom", "radius"]);
super(Object.assign(Object.assign({}, otherOptions), { simpleThreeOptions: Object.assign(Object.assign({}, otherOptions.simpleThreeOptions), { preventDefaultRender: true }) }));
this.heatmapObj3D = new three_1.Group();
this.planeGeometry = new three_1.PlaneGeometry(1, 1);
this.dom.className = `${(_a = this.dom.className) !== null && _a !== void 0 ? _a : ''} three-heatmap-layer`;
this.dom.style.pointerEvents = 'none';
this.dataPrecision = dataPrecision;
this.opacity = opacity;
this.dataOrigin = data;
this.min = min;
this.max = max;
this.radius = radius;
this.minZoom = minZoom;
this.maxZoom = maxZoom;
this.z = z;
// 构造用于热力图纹理的渐变圆
const canvas2d = document.createElement('canvas');
canvas2d.width = 100;
canvas2d.height = 100;
const ctx = canvas2d.getContext('2d');
if (ctx) {
const grd = ctx.createRadialGradient(50, 50, 0, 50, 50, 50);
grd.addColorStop(0, 'rgba(255,255,255,1');
grd.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 100, 100);
}
this.heatmapTexture = new three_1.CanvasTexture(canvas2d);
const { renderer, scene, camera } = this.three;
scene.add(this.heatmapObj3D);
// 后期渲染
this.composer = new EffectComposer_1.EffectComposer(renderer);
this.renderPass = new RenderPass_1.RenderPass(scene, camera);
this.composer.addPass(this.renderPass);
this.three.on('render', () => {
this.composer.render();
});
this.setHeatmapColor(colors, colorPrecision);
}
/**
* 设置热力图颜色
* @param colors 热力值对应的颜色
* @param precision 热力图颜色精度
*/
setHeatmapColor(colors, precision = 100) {
const canvasColor = document.createElement('canvas');
canvasColor.width = precision;
canvasColor.height = 1;
const ctxColor = canvasColor.getContext('2d');
if (ctxColor) {
const grd = ctxColor.createLinearGradient(0, 0, precision, 0);
colors.forEach(([percent, color], index) => {
grd.addColorStop(percent, color);
});
ctxColor.fillStyle = grd;
ctxColor.fillRect(0, 0, precision, 10);
}
const HeatmapShader = {
uniforms: {
tDiffuse: { value: null },
opacity: { value: this.opacity },
colorTexture: { value: new three_1.CanvasTexture(canvasColor) },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */ `
uniform float opacity;
uniform sampler2D tDiffuse;
uniform sampler2D colorTexture;
varying vec2 vUv;
void main() {
// 得到一个 rgba 颜色
vec4 texel = texture2D( tDiffuse, vUv );
// 以 alpha 值作为热力
float alpha = texel.a;
// 从色阶表中取得该热力对应的颜色
vec4 color = texture2D( colorTexture, vec2( alpha, 0 ));
// 过滤透明度特别低的区域(否则热力图边界会出现白边)
gl_FragColor = opacity * step(0.04, alpha) * color;
}`,
};
if (this.shaderPass) {
this.composer.removePass(this.shaderPass);
if (this.shaderPass.fsQuad) {
// @ts-expect-error fsQuad can dispose
this.shaderPass.fsQuad.dispose();
}
}
this.shaderPass = new ShaderPass_1.ShaderPass(HeatmapShader);
this.composer.addPass(this.shaderPass);
this.composer.render();
}
createObject() {
var _a;
if (this.map && this.dataOrigin) {
const { threeCenter } = this.map;
if (threeCenter) {
// 计算最大值
let max = (_a = this.max) !== null && _a !== void 0 ? _a : 100;
if (max == null) {
max = this.dataOrigin.reduce((v, { value }) => Math.max(v, Number.isNaN(value) ? -Infinity : value), 0);
}
const precision = Math.max(1, this.dataPrecision);
const pointsArray = new Array(precision).fill([])
.map(() => []);
// 遍历数据,并归类
this.dataOrigin.forEach(({ lng, lat, value }) => {
const [x, y] = (0, coord_1.lonLat2Mercator)([lng, lat]).map((value, index) => value - threeCenter[index]);
const percent = Math.max(0, Math.min(1, (value - this.min) / max));
const index = Math.round(percent * precision);
if (pointsArray[index]) {
pointsArray[index].push({ x, y });
}
});
this.heatmapObj3D.clear();
// 创建 不同热力值的 Mesh
pointsArray.forEach((points, index) => {
const opacity = (index + 1) / precision;
const mesh = new three_1.InstancedMesh(this.planeGeometry, new three_1.MeshBasicMaterial({
opacity,
blending: three_1.AdditiveBlending,
depthTest: false,
transparent: true,
side: three_1.DoubleSide,
map: this.heatmapTexture,
}), points.length);
const obj = new three_1.Object3D();
// 设置每个点的位置
points.forEach(({ x, y }, i) => {
obj.position.set(x, y, this.z);
obj.updateMatrix();
mesh.setMatrixAt(i, obj.matrix);
});
this.heatmapObj3D.add(mesh);
});
this.composer.render();
}
}
}
setData(data, max, min = 0) {
this.dataOrigin = data;
this.max = max;
this.min = min;
this.createObject();
}
setMin(min = 0) {
this.min = min;
this.createObject();
}
setMax(max = 0) {
this.max = max;
this.createObject();
}
setMinZoom(minZoom) {
this.minZoom = minZoom;
this.updatePositionDebounce();
}
setMaxZoom(maxZoom) {
this.maxZoom = maxZoom;
this.updatePositionDebounce();
}
setOpacity(opacity = 0.9) {
var _a;
this.opacity = opacity;
if ((_a = this.shaderPass) === null || _a === void 0 ? void 0 : _a.material.uniforms.opacity) {
this.shaderPass.material.uniforms.opacity.value = opacity;
}
this.updatePosition();
}
setRadius(radius) {
this.radius = radius;
this.updatePosition();
}
updatePosition() {
if (this.map) {
let { zoom } = this.map.getViewport();
if (this.minZoom)
zoom = Math.max(this.minZoom, zoom);
if (this.maxZoom)
zoom = Math.min(this.maxZoom, zoom);
const scale = (this.radius * coord_1.MercatorMax * 2) / 256 / Math.pow(2, zoom);
if (!this.planeGeometry.boundingBox) {
this.planeGeometry.computeBoundingBox();
}
const { boundingBox } = this.planeGeometry;
if (boundingBox) {
const originScale = boundingBox.max.x - boundingBox.min.x;
const scaleDiff = scale / originScale;
if (Math.abs(scaleDiff - 1) > 0.03) {
this.planeGeometry.scale(scaleDiff, scaleDiff, 1);
this.planeGeometry.computeBoundingBox();
}
}
}
super.updatePosition();
}
setMap(map) {
super.setMap(map);
this.createObject();
}
add() {
throw new Error('ThreeHeatmapLayer can not add markers');
}
destroy() {
super.destroy();
this.three.dispose();
if (this.shaderPass) {
this.composer.removePass(this.shaderPass);
if (this.shaderPass.fsQuad) {
// @ts-expect-error fsQuad can dispose
this.shaderPass.fsQuad.dispose();
}
}
if (this.renderPass) {
this.composer.removePass(this.renderPass);
}
}
}
exports.default = ThreeHeatmapLayer;