SelectController.js 9.17 KB
/**
 * Box selection tool.
 *
 * @module echarts/component/helper/SelectController
 */

define(function (require) {

    var Eventful = require('zrender/mixin/Eventful');
    var zrUtil = require('zrender/core/util');
    var graphic = require('../../util/graphic');
    var bind = zrUtil.bind;
    var each = zrUtil.each;
    var mathMin = Math.min;
    var mathMax = Math.max;
    var mathPow = Math.pow;

    var COVER_Z = 10000;
    var UNSELECT_THRESHOLD = 2;
    var EVENTS = ['mousedown', 'mousemove', 'mouseup'];

    /**
     * @alias module:echarts/component/helper/SelectController
     * @constructor
     * @mixin {module:zrender/mixin/Eventful}
     *
     * @param {string} type 'line', 'rect'
     * @param {module:zrender/zrender~ZRender} zr
     * @param {Object} [opt]
     * @param {number} [opt.width]
     * @param {number} [opt.lineWidth]
     * @param {string} [opt.stroke]
     * @param {string} [opt.fill]
     */
    function SelectController(type, zr, opt) {

        Eventful.call(this);

        /**
         * @type {string}
         * @readOnly
         */
        this.type = type;

        /**
         * @type {module:zrender/zrender~ZRender}
         */
        this.zr = zr;

        /**
         * @type {Object}
         * @readOnly
         */
        this.opt = zrUtil.clone(opt);

        /**
         * @type {module:zrender/container/Group}
         * @readOnly
         */
        this.group = new graphic.Group();

        /**
         * @type {module:zrender/core/BoundingRect}
         */
        this._containerRect = null;

        /**
         * @type {Array.<nubmer>}
         * @private
         */
        this._track = [];

        /**
         * @type {boolean}
         */
        this._dragging;

        /**
         * @type {module:zrender/Element}
         * @private
         */
        this._cover;

        /**
         * @type {boolean}
         * @private
         */
        this._disabled = true;

        /**
         * @type {Object}
         * @private
         */
        this._handlers = {
            mousedown: bind(mousedown, this),
            mousemove: bind(mousemove, this),
            mouseup: bind(mouseup, this)
        };

        each(EVENTS, function (eventName) {
            this.zr.on(eventName, this._handlers[eventName]);
        }, this);
    }

    SelectController.prototype = {

        constructor: SelectController,

        /**
         * @param {module:zrender/mixin/Transformable} container
         * @param {module:zrender/core/BoundingRect|boolean} [rect] If not specified,
         *                                                  use container.getBoundingRect().
         *                                                  If false, do not use containerRect.
         */
        enable: function (container, rect) {

            this._disabled = false;

            // Remove from old container.
            removeGroup.call(this);

            // boundingRect will change when dragging, so we have
            // to keep initial boundingRect.
            this._containerRect = rect !== false
            ? (rect || container.getBoundingRect()) : null;

            // Add to new container.
            container.add(this.group);
        },

        /**
         * Update cover location.
         * @param {Array.<number>|Object} ranges If null/undefined, remove cover.
         */
        update: function (ranges) {
            // TODO
            // Only support one interval yet.
            renderCover.call(this, ranges && zrUtil.clone(ranges));
        },

        disable: function () {
            this._disabled = true;

            removeGroup.call(this);
        },

        dispose: function () {
            this.disable();

            each(EVENTS, function (eventName) {
                this.zr.off(eventName, this._handlers[eventName]);
            }, this);
        }
    };


    zrUtil.mixin(SelectController, Eventful);

    function updateZ(group) {
        group.traverse(function (el) {
            el.z = COVER_Z;
        });
    }

    function isInContainer(x, y) {
        var localPos = this.group.transformCoordToLocal(x, y);
        return !this._containerRect
            || this._containerRect.contain(localPos[0], localPos[1]);
    }

    function preventDefault(e) {
        var rawE = e.event;
        rawE.preventDefault && rawE.preventDefault();
    }

    function mousedown(e) {
        if (this._disabled || (e.target && e.target.draggable)) {
            return;
        }

        preventDefault(e);

        var x = e.offsetX;
        var y = e.offsetY;

        if (isInContainer.call(this, x, y)) {
            this._dragging = true;
            this._track = [[x, y]];
        }
    }

    function mousemove(e) {
        if (!this._dragging || this._disabled) {
            return;
        }

        preventDefault(e);

        updateViewByCursor.call(this, e);
    }

    function mouseup(e) {
        if (!this._dragging || this._disabled) {
            return;
        }

        preventDefault(e);

        updateViewByCursor.call(this, e, true);

        this._dragging = false;
        this._track = [];
    }

    function updateViewByCursor(e, isEnd) {
        var x = e.offsetX;
        var y = e.offsetY;

        if (isInContainer.call(this, x, y)) {
            this._track.push([x, y]);

            // Create or update cover.
            var ranges = shouldShowCover.call(this)
                ? coverRenderers[this.type].getRanges.call(this)
                // Remove cover.
                : [];

            renderCover.call(this, ranges);

            this.trigger('selected', zrUtil.clone(ranges));

            if (isEnd) {
                this.trigger('selectEnd', zrUtil.clone(ranges));
            }
        }
    }

    function shouldShowCover() {
        var track = this._track;

        if (!track.length) {
            return false;
        }

        var p2 = track[track.length - 1];
        var p1 = track[0];
        var dx = p2[0] - p1[0];
        var dy = p2[1] - p1[1];
        var dist = mathPow(dx * dx + dy * dy, 0.5);

        return dist > UNSELECT_THRESHOLD;
    }

    function renderCover(ranges) {
        var coverRenderer = coverRenderers[this.type];

        if (ranges && ranges.length) {
            if (!this._cover) {
                this._cover = coverRenderer.create.call(this);
                this.group.add(this._cover);
            }
            coverRenderer.update.call(this, ranges);
        }
        else {
            this.group.remove(this._cover);
            this._cover = null;
        }

        updateZ(this.group);
    }

    function removeGroup() {
        // container may 'removeAll' outside.
        var group = this.group;
        var container = group.parent;
        if (container) {
            container.remove(group);
        }
    }

    function createRectCover() {
        var opt = this.opt;
        return new graphic.Rect({
            // FIXME
            // customize style.
            style: {
                stroke: opt.stroke,
                fill: opt.fill,
                lineWidth: opt.lineWidth,
                opacity: opt.opacity
            }
        });
    }

    function getLocalTrack() {
        return zrUtil.map(this._track, function (point) {
            return this.group.transformCoordToLocal(point[0], point[1]);
        }, this);
    }

    function getLocalTrackEnds() {
        var localTrack = getLocalTrack.call(this);
        var tail = localTrack.length - 1;
        tail < 0 && (tail = 0);
        return [localTrack[0], localTrack[tail]];
    }

    /**
     * key: this.type
     * @type {Object}
     */
    var coverRenderers = {

        line: {

            create: createRectCover,

            getRanges: function () {
                var ends = getLocalTrackEnds.call(this);
                var min = mathMin(ends[0][0], ends[1][0]);
                var max = mathMax(ends[0][0], ends[1][0]);

                return [[min, max]];
            },

            update: function (ranges) {
                var range = ranges[0];
                var width = this.opt.width;
                this._cover.setShape({
                    x: range[0],
                    y: -width / 2,
                    width: range[1] - range[0],
                    height: width
                });
            }
        },

        rect: {

            create: createRectCover,

            getRanges: function () {
                var ends = getLocalTrackEnds.call(this);

                var min = [
                    mathMin(ends[1][0], ends[0][0]),
                    mathMin(ends[1][1], ends[0][1])
                ];
                var max = [
                    mathMax(ends[1][0], ends[0][0]),
                    mathMax(ends[1][1], ends[0][1])
                ];

                return [[
                    [min[0], max[0]], // x range
                    [min[1], max[1]] // y range
                ]];
            },

            update: function (ranges) {
                var range = ranges[0];
                this._cover.setShape({
                    x: range[0][0],
                    y: range[1][0],
                    width: range[0][1] - range[0][0],
                    height: range[1][1] - range[1][0]
                });
            }
        }
    };

    return SelectController;
});