FlowGLView.js 12.8 KB
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();
    }
});