332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
|
|
import * as echarts from 'echarts/lib/echarts';
|
||
|
|
import graphicGL from '../../util/graphicGL';
|
||
|
|
import retrieve from '../../util/retrieve';
|
||
|
|
import ViewGL from '../../core/ViewGL';
|
||
|
|
import VectorFieldParticleSurface from './VectorFieldParticleSurface'; // TODO 百度地图不是 linear 的
|
||
|
|
|
||
|
|
export default echarts.ChartView.extend({
|
||
|
|
type: 'flowGL',
|
||
|
|
__ecgl__: true,
|
||
|
|
init: function (ecModel, api) {
|
||
|
|
this.viewGL = new ViewGL('orthographic');
|
||
|
|
this.groupGL = new graphicGL.Node();
|
||
|
|
this.viewGL.add(this.groupGL);
|
||
|
|
this._particleSurface = new VectorFieldParticleSurface();
|
||
|
|
var planeMesh = new graphicGL.Mesh({
|
||
|
|
geometry: new graphicGL.PlaneGeometry(),
|
||
|
|
material: new graphicGL.Material({
|
||
|
|
shader: new graphicGL.Shader({
|
||
|
|
vertex: graphicGL.Shader.source('ecgl.color.vertex'),
|
||
|
|
fragment: graphicGL.Shader.source('ecgl.color.fragment')
|
||
|
|
}),
|
||
|
|
// Must enable blending and multiply alpha.
|
||
|
|
// Or premultipliedAlpha will let the alpha useless.
|
||
|
|
transparent: true
|
||
|
|
})
|
||
|
|
});
|
||
|
|
planeMesh.material.enableTexture('diffuseMap');
|
||
|
|
this.groupGL.add(planeMesh);
|
||
|
|
this._planeMesh = planeMesh;
|
||
|
|
},
|
||
|
|
render: function (seriesModel, ecModel, api) {
|
||
|
|
var particleSurface = this._particleSurface; // Set particleType before set others.
|
||
|
|
|
||
|
|
particleSurface.setParticleType(seriesModel.get('particleType'));
|
||
|
|
particleSurface.setSupersampling(seriesModel.get('supersampling'));
|
||
|
|
|
||
|
|
this._updateData(seriesModel, api);
|
||
|
|
|
||
|
|
this._updateCamera(api.getWidth(), api.getHeight(), api.getDevicePixelRatio());
|
||
|
|
|
||
|
|
var particleDensity = retrieve.firstNotNull(seriesModel.get('particleDensity'), 128);
|
||
|
|
particleSurface.setParticleDensity(particleDensity, particleDensity);
|
||
|
|
var planeMesh = this._planeMesh;
|
||
|
|
var time = +new Date();
|
||
|
|
var self = this;
|
||
|
|
var firstFrame = true;
|
||
|
|
planeMesh.__percent = 0;
|
||
|
|
planeMesh.stopAnimation();
|
||
|
|
planeMesh.animate('', {
|
||
|
|
loop: true
|
||
|
|
}).when(100000, {
|
||
|
|
__percent: 1
|
||
|
|
}).during(function () {
|
||
|
|
var timeNow = +new Date();
|
||
|
|
var dTime = Math.min(timeNow - time, 20);
|
||
|
|
time = time + dTime;
|
||
|
|
|
||
|
|
if (self._renderer) {
|
||
|
|
particleSurface.update(self._renderer, api, dTime / 1000, firstFrame);
|
||
|
|
planeMesh.material.set('diffuseMap', particleSurface.getSurfaceTexture()); // planeMesh.material.set('diffuseMap', self._particleSurface.vectorFieldTexture);
|
||
|
|
}
|
||
|
|
|
||
|
|
firstFrame = false;
|
||
|
|
}).start();
|
||
|
|
var itemStyleModel = seriesModel.getModel('itemStyle');
|
||
|
|
var color = graphicGL.parseColor(itemStyleModel.get('color'));
|
||
|
|
color[3] *= retrieve.firstNotNull(itemStyleModel.get('opacity'), 1);
|
||
|
|
planeMesh.material.set('color', color);
|
||
|
|
particleSurface.setColorTextureImage(seriesModel.get('colorTexture'), api);
|
||
|
|
particleSurface.setParticleSize(seriesModel.get('particleSize'));
|
||
|
|
particleSurface.particleSpeedScaling = seriesModel.get('particleSpeed');
|
||
|
|
particleSurface.motionBlurFactor = 1.0 - Math.pow(0.1, seriesModel.get('particleTrail'));
|
||
|
|
},
|
||
|
|
updateTransform: function (seriesModel, ecModel, api) {
|
||
|
|
this._updateData(seriesModel, api);
|
||
|
|
},
|
||
|
|
afterRender: function (globeModel, ecModel, api, layerGL) {
|
||
|
|
var renderer = layerGL.renderer;
|
||
|
|
this._renderer = renderer;
|
||
|
|
},
|
||
|
|
_updateData: function (seriesModel, api) {
|
||
|
|
var coordSys = seriesModel.coordinateSystem;
|
||
|
|
var dims = coordSys.dimensions.map(function (coordDim) {
|
||
|
|
return seriesModel.coordDimToDataDim(coordDim)[0];
|
||
|
|
});
|
||
|
|
var data = seriesModel.getData();
|
||
|
|
var xExtent = data.getDataExtent(dims[0]);
|
||
|
|
var yExtent = data.getDataExtent(dims[1]);
|
||
|
|
var gridWidth = seriesModel.get('gridWidth');
|
||
|
|
var gridHeight = seriesModel.get('gridHeight');
|
||
|
|
|
||
|
|
if (gridWidth == null || gridWidth === 'auto') {
|
||
|
|
// TODO not accurate.
|
||
|
|
var aspect = (xExtent[1] - xExtent[0]) / (yExtent[1] - yExtent[0]);
|
||
|
|
gridWidth = Math.round(Math.sqrt(aspect * data.count()));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (gridHeight == null || gridHeight === 'auto') {
|
||
|
|
gridHeight = Math.ceil(data.count() / gridWidth);
|
||
|
|
}
|
||
|
|
|
||
|
|
var vectorFieldTexture = this._particleSurface.vectorFieldTexture; // Half Float needs Uint16Array
|
||
|
|
|
||
|
|
var pixels = vectorFieldTexture.pixels;
|
||
|
|
|
||
|
|
if (!pixels || pixels.length !== gridHeight * gridWidth * 4) {
|
||
|
|
pixels = vectorFieldTexture.pixels = new Float32Array(gridWidth * gridHeight * 4);
|
||
|
|
} else {
|
||
|
|
for (var i = 0; i < pixels.length; i++) {
|
||
|
|
pixels[i] = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var maxMag = 0;
|
||
|
|
var minMag = Infinity;
|
||
|
|
var points = new Float32Array(data.count() * 2);
|
||
|
|
var offset = 0;
|
||
|
|
var bbox = [[Infinity, Infinity], [-Infinity, -Infinity]];
|
||
|
|
data.each([dims[0], dims[1], 'vx', 'vy'], function (x, y, vx, vy) {
|
||
|
|
var pt = coordSys.dataToPoint([x, y]);
|
||
|
|
points[offset++] = pt[0];
|
||
|
|
points[offset++] = pt[1];
|
||
|
|
bbox[0][0] = Math.min(pt[0], bbox[0][0]);
|
||
|
|
bbox[0][1] = Math.min(pt[1], bbox[0][1]);
|
||
|
|
bbox[1][0] = Math.max(pt[0], bbox[1][0]);
|
||
|
|
bbox[1][1] = Math.max(pt[1], bbox[1][1]);
|
||
|
|
var mag = Math.sqrt(vx * vx + vy * vy);
|
||
|
|
maxMag = Math.max(maxMag, mag);
|
||
|
|
minMag = Math.min(minMag, mag);
|
||
|
|
});
|
||
|
|
data.each(['vx', 'vy'], function (vx, vy, i) {
|
||
|
|
var xPix = Math.round((points[i * 2] - bbox[0][0]) / (bbox[1][0] - bbox[0][0]) * (gridWidth - 1));
|
||
|
|
var yPix = gridHeight - 1 - Math.round((points[i * 2 + 1] - bbox[0][1]) / (bbox[1][1] - bbox[0][1]) * (gridHeight - 1));
|
||
|
|
var idx = (yPix * gridWidth + xPix) * 4;
|
||
|
|
pixels[idx] = vx / maxMag * 0.5 + 0.5;
|
||
|
|
pixels[idx + 1] = vy / maxMag * 0.5 + 0.5;
|
||
|
|
pixels[idx + 3] = 1;
|
||
|
|
});
|
||
|
|
vectorFieldTexture.width = gridWidth;
|
||
|
|
vectorFieldTexture.height = gridHeight;
|
||
|
|
|
||
|
|
if (seriesModel.get('coordinateSystem') === 'bmap') {
|
||
|
|
this._fillEmptyPixels(vectorFieldTexture);
|
||
|
|
}
|
||
|
|
|
||
|
|
vectorFieldTexture.dirty();
|
||
|
|
|
||
|
|
this._updatePlanePosition(bbox[0], bbox[1], seriesModel, api);
|
||
|
|
|
||
|
|
this._updateGradientTexture(data.getVisual('visualMeta'), [minMag, maxMag]);
|
||
|
|
},
|
||
|
|
// PENDING Use grid mesh ? or delaunay triangulation?
|
||
|
|
_fillEmptyPixels: function (texture) {
|
||
|
|
var pixels = texture.pixels;
|
||
|
|
var width = texture.width;
|
||
|
|
var height = texture.height;
|
||
|
|
|
||
|
|
function fetchPixel(x, y, rg) {
|
||
|
|
x = Math.max(Math.min(x, width - 1), 0);
|
||
|
|
y = Math.max(Math.min(y, height - 1), 0);
|
||
|
|
var idx = (y * (width - 1) + x) * 4;
|
||
|
|
|
||
|
|
if (pixels[idx + 3] === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
rg[0] = pixels[idx];
|
||
|
|
rg[1] = pixels[idx + 1];
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function addPixel(a, b, out) {
|
||
|
|
out[0] = a[0] + b[0];
|
||
|
|
out[1] = a[1] + b[1];
|
||
|
|
}
|
||
|
|
|
||
|
|
var center = [],
|
||
|
|
left = [],
|
||
|
|
right = [],
|
||
|
|
top = [],
|
||
|
|
bottom = [];
|
||
|
|
var weight = 0;
|
||
|
|
|
||
|
|
for (var y = 0; y < height; y++) {
|
||
|
|
for (var x = 0; x < width; x++) {
|
||
|
|
var idx = (y * (width - 1) + x) * 4;
|
||
|
|
|
||
|
|
if (pixels[idx + 3] === 0) {
|
||
|
|
weight = center[0] = center[1] = 0;
|
||
|
|
|
||
|
|
if (fetchPixel(x - 1, y, left)) {
|
||
|
|
weight++;
|
||
|
|
addPixel(left, center, center);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fetchPixel(x + 1, y, right)) {
|
||
|
|
weight++;
|
||
|
|
addPixel(right, center, center);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fetchPixel(x, y - 1, top)) {
|
||
|
|
weight++;
|
||
|
|
addPixel(top, center, center);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fetchPixel(x, y + 1, bottom)) {
|
||
|
|
weight++;
|
||
|
|
addPixel(bottom, center, center);
|
||
|
|
}
|
||
|
|
|
||
|
|
center[0] /= weight;
|
||
|
|
center[1] /= weight; // PENDING If overwrite. bilinear interpolation.
|
||
|
|
|
||
|
|
pixels[idx] = center[0];
|
||
|
|
pixels[idx + 1] = center[1];
|
||
|
|
}
|
||
|
|
|
||
|
|
pixels[idx + 3] = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
_updateGradientTexture: function (visualMeta, magExtent) {
|
||
|
|
if (!visualMeta || !visualMeta.length) {
|
||
|
|
this._particleSurface.setGradientTexture(null);
|
||
|
|
|
||
|
|
return;
|
||
|
|
} // TODO Different dimensions
|
||
|
|
|
||
|
|
|
||
|
|
this._gradientTexture = this._gradientTexture || new graphicGL.Texture2D({
|
||
|
|
image: document.createElement('canvas')
|
||
|
|
});
|
||
|
|
var gradientTexture = this._gradientTexture;
|
||
|
|
var canvas = gradientTexture.image;
|
||
|
|
canvas.width = 200;
|
||
|
|
canvas.height = 1;
|
||
|
|
var ctx = canvas.getContext('2d');
|
||
|
|
var gradient = ctx.createLinearGradient(0, 0.5, canvas.width, 0.5);
|
||
|
|
visualMeta[0].stops.forEach(function (stop) {
|
||
|
|
var offset;
|
||
|
|
|
||
|
|
if (magExtent[1] === magExtent[0]) {
|
||
|
|
offset = 0;
|
||
|
|
} else {
|
||
|
|
offset = stop.value / magExtent[1];
|
||
|
|
offset = Math.min(Math.max(offset, 0), 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
gradient.addColorStop(offset, stop.color);
|
||
|
|
});
|
||
|
|
ctx.fillStyle = gradient;
|
||
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
|
gradientTexture.dirty();
|
||
|
|
|
||
|
|
this._particleSurface.setGradientTexture(this._gradientTexture);
|
||
|
|
},
|
||
|
|
_updatePlanePosition: function (leftTop, rightBottom, seriesModel, api) {
|
||
|
|
var limitedResult = this._limitInViewportAndFullFill(leftTop, rightBottom, seriesModel, api);
|
||
|
|
|
||
|
|
leftTop = limitedResult.leftTop;
|
||
|
|
rightBottom = limitedResult.rightBottom;
|
||
|
|
|
||
|
|
this._particleSurface.setRegion(limitedResult.region);
|
||
|
|
|
||
|
|
this._planeMesh.position.set((leftTop[0] + rightBottom[0]) / 2, api.getHeight() - (leftTop[1] + rightBottom[1]) / 2, 0);
|
||
|
|
|
||
|
|
var width = rightBottom[0] - leftTop[0];
|
||
|
|
var height = rightBottom[1] - leftTop[1];
|
||
|
|
|
||
|
|
this._planeMesh.scale.set(width / 2, height / 2, 1);
|
||
|
|
|
||
|
|
this._particleSurface.resize(Math.max(Math.min(width, 2048), 1), Math.max(Math.min(height, 2048), 1));
|
||
|
|
|
||
|
|
if (this._renderer) {
|
||
|
|
this._particleSurface.clearFrame(this._renderer);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
_limitInViewportAndFullFill: function (leftTop, rightBottom, seriesModel, api) {
|
||
|
|
var newLeftTop = [Math.max(leftTop[0], 0), Math.max(leftTop[1], 0)];
|
||
|
|
var newRightBottom = [Math.min(rightBottom[0], api.getWidth()), Math.min(rightBottom[1], api.getHeight())]; // Tiliing in lng orientation.
|
||
|
|
|
||
|
|
if (seriesModel.get('coordinateSystem') === 'bmap') {
|
||
|
|
var lngRange = seriesModel.getData().getDataExtent(seriesModel.coordDimToDataDim('lng')[0]); // PENDING, consider grid density
|
||
|
|
|
||
|
|
var isContinuous = Math.floor(lngRange[1] - lngRange[0]) >= 359;
|
||
|
|
|
||
|
|
if (isContinuous) {
|
||
|
|
if (newLeftTop[0] > 0) {
|
||
|
|
newLeftTop[0] = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (newRightBottom[0] < api.getWidth()) {
|
||
|
|
newRightBottom[0] = api.getWidth();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var width = rightBottom[0] - leftTop[0];
|
||
|
|
var height = rightBottom[1] - leftTop[1];
|
||
|
|
var newWidth = newRightBottom[0] - newLeftTop[0];
|
||
|
|
var newHeight = newRightBottom[1] - newLeftTop[1];
|
||
|
|
var region = [(newLeftTop[0] - leftTop[0]) / width, 1.0 - newHeight / height - (newLeftTop[1] - leftTop[1]) / height, newWidth / width, newHeight / height];
|
||
|
|
return {
|
||
|
|
leftTop: newLeftTop,
|
||
|
|
rightBottom: newRightBottom,
|
||
|
|
region: region
|
||
|
|
};
|
||
|
|
},
|
||
|
|
_updateCamera: function (width, height, dpr) {
|
||
|
|
this.viewGL.setViewport(0, 0, width, height, dpr);
|
||
|
|
var camera = this.viewGL.camera; // FIXME bottom can't be larger than top
|
||
|
|
|
||
|
|
camera.left = camera.bottom = 0;
|
||
|
|
camera.top = height;
|
||
|
|
camera.right = width;
|
||
|
|
camera.near = 0;
|
||
|
|
camera.far = 100;
|
||
|
|
camera.position.z = 10;
|
||
|
|
},
|
||
|
|
remove: function () {
|
||
|
|
this._planeMesh.stopAnimation();
|
||
|
|
|
||
|
|
this.groupGL.removeAll();
|
||
|
|
},
|
||
|
|
dispose: function () {
|
||
|
|
if (this._renderer) {
|
||
|
|
this._particleSurface.dispose(this._renderer);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.groupGL.removeAll();
|
||
|
|
}
|
||
|
|
});
|