| /* | 
 | * Licensed to the Apache Software Foundation (ASF) under one | 
 | * or more contributor license agreements.  See the NOTICE file | 
 | * distributed with this work for additional information | 
 | * regarding copyright ownership.  The ASF licenses this file | 
 | * to you under the Apache License, Version 2.0 (the | 
 | * "License"); you may not use this file except in compliance | 
 | * with the License.  You may obtain a copy of the License at | 
 | * | 
 | *   http://www.apache.org/licenses/LICENSE-2.0 | 
 | * | 
 | * Unless required by applicable law or agreed to in writing, | 
 | * software distributed under the License is distributed on an | 
 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
 | * KIND, either express or implied.  See the License for the | 
 | * specific language governing permissions and limitations | 
 | * under the License. | 
 | */ | 
 |  | 
 |  | 
 | import {curry, each, map, bind, merge, clone, defaults, assert} from 'zrender/src/core/util'; | 
 | import Eventful from 'zrender/src/core/Eventful'; | 
 | import * as graphic from '../../util/graphic'; | 
 | import * as interactionMutex from './interactionMutex'; | 
 | import DataDiffer from '../../data/DataDiffer'; | 
 | import { Dictionary } from '../../util/types'; | 
 | import { ZRenderType } from 'zrender/src/zrender'; | 
 | import { ElementEvent } from 'zrender/src/Element'; | 
 | import * as matrix from 'zrender/src/core/matrix'; | 
 | import Displayable from 'zrender/src/graphic/Displayable'; | 
 | import { PathStyleProps } from 'zrender/src/graphic/Path'; | 
 |  | 
 |  | 
 | /** | 
 |  * BrushController not only used in "brush component", | 
 |  * but also used in "tooltip DataZoom", and other possible | 
 |  * futher brush behavior related scenarios. | 
 |  * So `BrushController` should not depends on "brush component model". | 
 |  */ | 
 |  | 
 |  | 
 | export type BrushType = 'polygon' | 'rect' | 'lineX' | 'lineY'; | 
 | /** | 
 |  * Only for drawing (after enabledBrush). | 
 |  * 'line', 'rect', 'polygon' or false | 
 |  * If passing false/null/undefined, disable brush. | 
 |  * If passing 'auto', determined by panel.defaultBrushType | 
 |  */ | 
 | export type BrushTypeUncertain = BrushType | false | 'auto'; | 
 |  | 
 | export type BrushMode = 'single' | 'multiple'; | 
 | // MinMax: Range of linear brush. | 
 | // MinMax[]: Range of multi-dimension like rect/polygon, which is a MinMax | 
 | //     list for each dimension of the coord sys. For example: | 
 | //     cartesian: [[xMin, xMax], [yMin, yMax]] | 
 | //     geo: [[lngMin, lngMin], [latMin, latMax]] | 
 | export type BrushDimensionMinMax = number[]; | 
 | export type BrushAreaRange = BrushDimensionMinMax | BrushDimensionMinMax[]; | 
 |  | 
 | export interface BrushCoverConfig { | 
 |     // Mandatory. determine how to convert to/from coord('rect' or 'polygon' or 'lineX/Y') | 
 |     brushType: BrushType; | 
 |     // Can be specified by user to map covers in `updateCovers` | 
 |     // in `dispatchAction({type: 'brush', areas: [{id: ...}, ...]})` | 
 |     id?: string; | 
 |     // Range in global coordinate (pixel). | 
 |     range?: BrushAreaRange; | 
 |     // When create a new area by `updateCovers`, panelId should be specified. | 
 |     // If not null/undefined, means global panel. | 
 |     // Also see `BrushAreaParam['panelId']`. | 
 |     panelId?: string; | 
 |  | 
 |     brushMode?: BrushMode; | 
 |     // `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default. | 
 |     brushStyle?: Pick<PathStyleProps, BrushStyleKey>; | 
 |     transformable?: boolean; | 
 |     removeOnClick?: boolean; | 
 |     z?: number; | 
 | } | 
 |  | 
 | /** | 
 |  * `BrushAreaCreatorOption` input to brushModel via `setBrushOption`, | 
 |  * merge and convert to `BrushCoverCreatorConfig`. | 
 |  */ | 
 | export interface BrushCoverCreatorConfig extends Pick< | 
 |     BrushCoverConfig, | 
 |     'brushMode' | 'transformable' | 'removeOnClick' | 'brushStyle' | 'z' | 
 | > { | 
 |     brushType: BrushTypeUncertain; | 
 | } | 
 |  | 
 | type BrushStyleKey = | 
 |     'fill' | 
 |     | 'stroke' | 
 |     | 'lineWidth' | 
 |     | 'opacity' | 
 |     | 'shadowBlur' | 
 |     | 'shadowOffsetX' | 
 |     | 'shadowOffsetY' | 
 |     | 'shadowColor'; | 
 |  | 
 |  | 
 | const BRUSH_PANEL_GLOBAL = true as const; | 
 |  | 
 | export interface BrushPanelConfig { | 
 |     // mandatory. | 
 |     panelId: string; | 
 |     // mandatory. | 
 |     clipPath(localPoints: number[][], transform: matrix.MatrixArray): number[][]; | 
 |     // mandatory. | 
 |     isTargetByCursor(e: ElementEvent, localCursorPoint: number[], transform: matrix.MatrixArray): boolean; | 
 |     // optional, only used when brushType is 'auto'. | 
 |     defaultBrushType?: BrushType; | 
 |     // optional. | 
 |     getLinearBrushOtherExtent?(xyIndex: number): number[]; | 
 | } | 
 | // `true` represents global panel; | 
 | type BrushPanelConfigOrGlobal = BrushPanelConfig | typeof BRUSH_PANEL_GLOBAL; | 
 |  | 
 |  | 
 | interface BrushCover extends graphic.Group { | 
 |     __brushOption: BrushCoverConfig; | 
 | } | 
 |  | 
 | type Point = number[]; | 
 |  | 
 | const mathMin = Math.min; | 
 | const mathMax = Math.max; | 
 | const mathPow = Math.pow; | 
 |  | 
 | const COVER_Z = 10000; | 
 | const UNSELECT_THRESHOLD = 6; | 
 | const MIN_RESIZE_LINE_WIDTH = 6; | 
 | const MUTEX_RESOURCE_KEY = 'globalPan'; | 
 |  | 
 | type DirectionName = 'w' | 'e' | 'n' | 's'; | 
 | type DirectionNameSequence = DirectionName[]; | 
 |  | 
 | const DIRECTION_MAP = { | 
 |     w: [0, 0], | 
 |     e: [0, 1], | 
 |     n: [1, 0], | 
 |     s: [1, 1] | 
 | } as const; | 
 | const CURSOR_MAP = { | 
 |     w: 'ew', | 
 |     e: 'ew', | 
 |     n: 'ns', | 
 |     s: 'ns', | 
 |     ne: 'nesw', | 
 |     sw: 'nesw', | 
 |     nw: 'nwse', | 
 |     se: 'nwse' | 
 | } as const; | 
 | const DEFAULT_BRUSH_OPT = { | 
 |     brushStyle: { | 
 |         lineWidth: 2, | 
 |         stroke: 'rgba(210,219,238,0.3)', | 
 |         fill: '#D2DBEE' | 
 |     }, | 
 |     transformable: true, | 
 |     brushMode: 'single', | 
 |     removeOnClick: false | 
 | }; | 
 |  | 
 | let baseUID = 0; | 
 |  | 
 | export interface BrushControllerEvents { | 
 |     brush: { | 
 |         areas: { | 
 |             brushType: BrushType; | 
 |             panelId: string; | 
 |             range: BrushAreaRange; | 
 |         }[]; | 
 |         isEnd: boolean; | 
 |         removeOnClick: boolean; | 
 |     } | 
 | } | 
 |  | 
 | /** | 
 |  * params: | 
 |  *     areas: Array.<Array>, coord relates to container group, | 
 |  *                             If no container specified, to global. | 
 |  *     opt { | 
 |  *         isEnd: boolean, | 
 |  *         removeOnClick: boolean | 
 |  *     } | 
 |  */ | 
 | class BrushController extends Eventful<{ | 
 |     [key in keyof BrushControllerEvents]: (params: BrushControllerEvents[key]) => void | undefined | 
 | }> { | 
 |  | 
 |     readonly group: graphic.Group; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _zr: ZRenderType; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _brushType: BrushTypeUncertain; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      * Only for drawing (after enabledBrush). | 
 |      */ | 
 |     _brushOption: BrushCoverCreatorConfig; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      * Key: panelId | 
 |      */ | 
 |     _panels: Dictionary<BrushPanelConfig>; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _track: number[][] = []; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _dragging: boolean; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _covers: BrushCover[] = []; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _creatingCover: BrushCover; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _creatingPanel: BrushPanelConfigOrGlobal; | 
 |  | 
 |     private _enableGlobalPan: boolean; | 
 |  | 
 |     private _mounted: boolean; | 
 |  | 
 |     /** | 
 |      * @internal | 
 |      */ | 
 |     _transform: matrix.MatrixArray; | 
 |  | 
 |     private _uid: string; | 
 |  | 
 |     private _handlers: { | 
 |         [eventName: string]: (this: BrushController, e: ElementEvent) => void | 
 |     } = {}; | 
 |  | 
 |  | 
 |     constructor(zr: ZRenderType) { | 
 |         super(); | 
 |  | 
 |         if (__DEV__) { | 
 |             assert(zr); | 
 |         } | 
 |  | 
 |         this._zr = zr; | 
 |  | 
 |         this.group = new graphic.Group(); | 
 |  | 
 |         this._uid = 'brushController_' + baseUID++; | 
 |  | 
 |         each(pointerHandlers, function (this: BrushController, handler, eventName) { | 
 |             this._handlers[eventName] = bind(handler, this); | 
 |         }, this); | 
 |     } | 
 |  | 
 |     /** | 
 |      * If set to `false`, select disabled. | 
 |      */ | 
 |     enableBrush(brushOption: Partial<BrushCoverCreatorConfig> | false): BrushController { | 
 |         if (__DEV__) { | 
 |             assert(this._mounted); | 
 |         } | 
 |  | 
 |         this._brushType && this._doDisableBrush(); | 
 |         (brushOption as Partial<BrushCoverCreatorConfig>).brushType && this._doEnableBrush( | 
 |             brushOption as Partial<BrushCoverCreatorConfig> | 
 |         ); | 
 |  | 
 |         return this; | 
 |     } | 
 |  | 
 |     private _doEnableBrush(brushOption: Partial<BrushCoverCreatorConfig>): void { | 
 |         const zr = this._zr; | 
 |  | 
 |         // Consider roam, which takes globalPan too. | 
 |         if (!this._enableGlobalPan) { | 
 |             interactionMutex.take(zr, MUTEX_RESOURCE_KEY, this._uid); | 
 |         } | 
 |  | 
 |         each(this._handlers, function (handler, eventName) { | 
 |             zr.on(eventName, handler); | 
 |         }); | 
 |  | 
 |         this._brushType = brushOption.brushType; | 
 |         this._brushOption = merge( | 
 |             clone(DEFAULT_BRUSH_OPT), brushOption, true | 
 |         ) as BrushCoverCreatorConfig; | 
 |     } | 
 |  | 
 |     private _doDisableBrush(): void { | 
 |         const zr = this._zr; | 
 |  | 
 |         interactionMutex.release(zr, MUTEX_RESOURCE_KEY, this._uid); | 
 |  | 
 |         each(this._handlers, function (handler, eventName) { | 
 |             zr.off(eventName, handler); | 
 |         }); | 
 |  | 
 |         this._brushType = this._brushOption = null; | 
 |     } | 
 |  | 
 |     /** | 
 |      * @param panelOpts If not pass, it is global brush. | 
 |      */ | 
 |     setPanels(panelOpts?: BrushPanelConfig[]): BrushController { | 
 |         if (panelOpts && panelOpts.length) { | 
 |             const panels = this._panels = {} as Dictionary<BrushPanelConfig>; | 
 |             each(panelOpts, function (panelOpts) { | 
 |                 panels[panelOpts.panelId] = clone(panelOpts); | 
 |             }); | 
 |         } | 
 |         else { | 
 |             this._panels = null; | 
 |         } | 
 |         return this; | 
 |     } | 
 |  | 
 |     mount(opt?: { | 
 |         enableGlobalPan?: boolean; | 
 |         x?: number; | 
 |         y?: number; | 
 |         rotation?: number; | 
 |         scaleX?: number; | 
 |         scaleY?: number | 
 |     }): BrushController { | 
 |         opt = opt || {}; | 
 |  | 
 |         if (__DEV__) { | 
 |             this._mounted = true; // should be at first. | 
 |         } | 
 |  | 
 |         this._enableGlobalPan = opt.enableGlobalPan; | 
 |  | 
 |         const thisGroup = this.group; | 
 |         this._zr.add(thisGroup); | 
 |  | 
 |         thisGroup.attr({ | 
 |             x: opt.x || 0, | 
 |             y: opt.y || 0, | 
 |             rotation: opt.rotation || 0, | 
 |             scaleX: opt.scaleX || 1, | 
 |             scaleY: opt.scaleY || 1 | 
 |         }); | 
 |         this._transform = thisGroup.getLocalTransform(); | 
 |  | 
 |         return this; | 
 |     } | 
 |  | 
 |     // eachCover(cb, context): void { | 
 |     //     each(this._covers, cb, context); | 
 |     // } | 
 |  | 
 |     /** | 
 |      * Update covers. | 
 |      * @param coverConfigList | 
 |      *        If coverConfigList is null/undefined, all covers removed. | 
 |      */ | 
 |     updateCovers(coverConfigList: BrushCoverConfig[]) { | 
 |         if (__DEV__) { | 
 |             assert(this._mounted); | 
 |         } | 
 |  | 
 |         coverConfigList = map(coverConfigList, function (coverConfig) { | 
 |             return merge(clone(DEFAULT_BRUSH_OPT), coverConfig, true); | 
 |         }) as BrushCoverConfig[]; | 
 |  | 
 |         const tmpIdPrefix = '\0-brush-index-'; | 
 |         const oldCovers = this._covers; | 
 |         const newCovers = this._covers = [] as BrushCover[]; | 
 |         const controller = this; | 
 |         const creatingCover = this._creatingCover; | 
 |  | 
 |         (new DataDiffer(oldCovers, coverConfigList, oldGetKey, getKey)) | 
 |             .add(addOrUpdate) | 
 |             .update(addOrUpdate) | 
 |             .remove(remove) | 
 |             .execute(); | 
 |  | 
 |         return this; | 
 |  | 
 |         function getKey(brushOption: BrushCoverConfig, index: number): string { | 
 |             return (brushOption.id != null ? brushOption.id : tmpIdPrefix + index) | 
 |                 + '-' + brushOption.brushType; | 
 |         } | 
 |  | 
 |         function oldGetKey(cover: BrushCover, index: number): string { | 
 |             return getKey(cover.__brushOption, index); | 
 |         } | 
 |  | 
 |         function addOrUpdate(newIndex: number, oldIndex?: number): void { | 
 |             const newBrushInternal = coverConfigList[newIndex]; | 
 |             // Consider setOption in event listener of brushSelect, | 
 |             // where updating cover when creating should be forbiden. | 
 |             if (oldIndex != null && oldCovers[oldIndex] === creatingCover) { | 
 |                 newCovers[newIndex] = oldCovers[oldIndex]; | 
 |             } | 
 |             else { | 
 |                 const cover = newCovers[newIndex] = oldIndex != null | 
 |                     ? ( | 
 |                         oldCovers[oldIndex].__brushOption = newBrushInternal, | 
 |                         oldCovers[oldIndex] | 
 |                     ) | 
 |                     : endCreating(controller, createCover(controller, newBrushInternal)); | 
 |                 updateCoverAfterCreation(controller, cover); | 
 |             } | 
 |         } | 
 |  | 
 |         function remove(oldIndex: number) { | 
 |             if (oldCovers[oldIndex] !== creatingCover) { | 
 |                 controller.group.remove(oldCovers[oldIndex]); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     unmount() { | 
 |         if (__DEV__) { | 
 |             if (!this._mounted) { | 
 |                 return; | 
 |             } | 
 |         } | 
 |  | 
 |         this.enableBrush(false); | 
 |  | 
 |         // container may 'removeAll' outside. | 
 |         clearCovers(this); | 
 |         this._zr.remove(this.group); | 
 |  | 
 |         if (__DEV__) { | 
 |             this._mounted = false; // should be at last. | 
 |         } | 
 |  | 
 |         return this; | 
 |     } | 
 |  | 
 |     dispose() { | 
 |         this.unmount(); | 
 |         this.off(); | 
 |     } | 
 | } | 
 |  | 
 |  | 
 | function createCover(controller: BrushController, brushOption: BrushCoverConfig): BrushCover { | 
 |     const cover = coverRenderers[brushOption.brushType].createCover(controller, brushOption); | 
 |     cover.__brushOption = brushOption; | 
 |     updateZ(cover, brushOption); | 
 |     controller.group.add(cover); | 
 |     return cover; | 
 | } | 
 |  | 
 | function endCreating(controller: BrushController, creatingCover: BrushCover): BrushCover { | 
 |     const coverRenderer = getCoverRenderer(creatingCover); | 
 |     if (coverRenderer.endCreating) { | 
 |         coverRenderer.endCreating(controller, creatingCover); | 
 |         updateZ(creatingCover, creatingCover.__brushOption); | 
 |     } | 
 |     return creatingCover; | 
 | } | 
 |  | 
 | function updateCoverShape(controller: BrushController, cover: BrushCover): void { | 
 |     const brushOption = cover.__brushOption; | 
 |     getCoverRenderer(cover).updateCoverShape( | 
 |         controller, cover, brushOption.range, brushOption | 
 |     ); | 
 | } | 
 |  | 
 | function updateZ(cover: BrushCover, brushOption: BrushCoverConfig): void { | 
 |     let z = brushOption.z; | 
 |     z == null && (z = COVER_Z); | 
 |     cover.traverse(function (el: Displayable) { | 
 |         el.z = z; | 
 |         el.z2 = z; // Consider in given container. | 
 |     }); | 
 | } | 
 |  | 
 | function updateCoverAfterCreation(controller: BrushController, cover: BrushCover): void { | 
 |     getCoverRenderer(cover).updateCommon(controller, cover); | 
 |     updateCoverShape(controller, cover); | 
 | } | 
 |  | 
 | function getCoverRenderer(cover: BrushCover): CoverRenderer { | 
 |     return coverRenderers[cover.__brushOption.brushType]; | 
 | } | 
 |  | 
 | // return target panel or `true` (means global panel) | 
 | function getPanelByPoint( | 
 |     controller: BrushController, | 
 |     e: ElementEvent, | 
 |     localCursorPoint: Point | 
 | ): BrushPanelConfigOrGlobal { | 
 |     const panels = controller._panels; | 
 |     if (!panels) { | 
 |         return BRUSH_PANEL_GLOBAL; // Global panel | 
 |     } | 
 |     let panel; | 
 |     const transform = controller._transform; | 
 |     each(panels, function (pn) { | 
 |         pn.isTargetByCursor(e, localCursorPoint, transform) && (panel = pn); | 
 |     }); | 
 |     return panel; | 
 | } | 
 |  | 
 | // Return a panel or true | 
 | function getPanelByCover(controller: BrushController, cover: BrushCover): BrushPanelConfigOrGlobal { | 
 |     const panels = controller._panels; | 
 |     if (!panels) { | 
 |         return BRUSH_PANEL_GLOBAL; // Global panel | 
 |     } | 
 |     const panelId = cover.__brushOption.panelId; | 
 |     // User may give cover without coord sys info, | 
 |     // which is then treated as global panel. | 
 |     return panelId != null ? panels[panelId] : BRUSH_PANEL_GLOBAL; | 
 | } | 
 |  | 
 | function clearCovers(controller: BrushController): boolean { | 
 |     const covers = controller._covers; | 
 |     const originalLength = covers.length; | 
 |     each(covers, function (cover) { | 
 |         controller.group.remove(cover); | 
 |     }, controller); | 
 |     covers.length = 0; | 
 |  | 
 |     return !!originalLength; | 
 | } | 
 |  | 
 | function trigger( | 
 |     controller: BrushController, | 
 |     opt: {isEnd?: boolean, removeOnClick?: boolean} | 
 | ): void { | 
 |     const areas = map(controller._covers, function (cover) { | 
 |         const brushOption = cover.__brushOption; | 
 |         const range = clone(brushOption.range); | 
 |         return { | 
 |             brushType: brushOption.brushType, | 
 |             panelId: brushOption.panelId, | 
 |             range: range | 
 |         }; | 
 |     }); | 
 |  | 
 |     controller.trigger('brush', { | 
 |         areas: areas, | 
 |         isEnd: !!opt.isEnd, | 
 |         removeOnClick: !!opt.removeOnClick | 
 |     }); | 
 | } | 
 |  | 
 | function shouldShowCover(controller: BrushController): boolean { | 
 |     const track = controller._track; | 
 |  | 
 |     if (!track.length) { | 
 |         return false; | 
 |     } | 
 |  | 
 |     const p2 = track[track.length - 1]; | 
 |     const p1 = track[0]; | 
 |     const dx = p2[0] - p1[0]; | 
 |     const dy = p2[1] - p1[1]; | 
 |     const dist = mathPow(dx * dx + dy * dy, 0.5); | 
 |  | 
 |     return dist > UNSELECT_THRESHOLD; | 
 | } | 
 |  | 
 | function getTrackEnds(track: Point[]): Point[] { | 
 |     let tail = track.length - 1; | 
 |     tail < 0 && (tail = 0); | 
 |     return [track[0], track[tail]]; | 
 | } | 
 |  | 
 | interface RectRangeConverter { | 
 |     toRectRange: (range: BrushAreaRange) => BrushDimensionMinMax[]; | 
 |     fromRectRange: (areaRange: BrushDimensionMinMax[]) => BrushAreaRange; | 
 | }; | 
 | function createBaseRectCover( | 
 |     rectRangeConverter: RectRangeConverter, | 
 |     controller: BrushController, | 
 |     brushOption: BrushCoverConfig, | 
 |     edgeNameSequences: DirectionNameSequence[] | 
 | ): BrushCover { | 
 |     const cover = new graphic.Group() as BrushCover; | 
 |  | 
 |     cover.add(new graphic.Rect({ | 
 |         name: 'main', | 
 |         style: makeStyle(brushOption), | 
 |         silent: true, | 
 |         draggable: true, | 
 |         cursor: 'move', | 
 |         drift: curry(driftRect, rectRangeConverter, controller, cover, ['n', 's', 'w', 'e']), | 
 |         ondragend: curry(trigger, controller, {isEnd: true}) | 
 |     })); | 
 |  | 
 |     each( | 
 |         edgeNameSequences, | 
 |         function (nameSequence) { | 
 |             cover.add(new graphic.Rect({ | 
 |                 name: nameSequence.join(''), | 
 |                 style: {opacity: 0}, | 
 |                 draggable: true, | 
 |                 silent: true, | 
 |                 invisible: true, | 
 |                 drift: curry(driftRect, rectRangeConverter, controller, cover, nameSequence), | 
 |                 ondragend: curry(trigger, controller, {isEnd: true}) | 
 |             })); | 
 |         } | 
 |     ); | 
 |  | 
 |     return cover; | 
 | } | 
 |  | 
 | function updateBaseRect( | 
 |     controller: BrushController, | 
 |     cover: BrushCover, | 
 |     localRange: BrushDimensionMinMax[], | 
 |     brushOption: BrushCoverConfig | 
 | ): void { | 
 |     const lineWidth = brushOption.brushStyle.lineWidth || 0; | 
 |     const handleSize = mathMax(lineWidth, MIN_RESIZE_LINE_WIDTH); | 
 |     const x = localRange[0][0]; | 
 |     const y = localRange[1][0]; | 
 |     const xa = x - lineWidth / 2; | 
 |     const ya = y - lineWidth / 2; | 
 |     const x2 = localRange[0][1]; | 
 |     const y2 = localRange[1][1]; | 
 |     const x2a = x2 - handleSize + lineWidth / 2; | 
 |     const y2a = y2 - handleSize + lineWidth / 2; | 
 |     const width = x2 - x; | 
 |     const height = y2 - y; | 
 |     const widtha = width + lineWidth; | 
 |     const heighta = height + lineWidth; | 
 |  | 
 |     updateRectShape(controller, cover, 'main', x, y, width, height); | 
 |  | 
 |     if (brushOption.transformable) { | 
 |         updateRectShape(controller, cover, 'w', xa, ya, handleSize, heighta); | 
 |         updateRectShape(controller, cover, 'e', x2a, ya, handleSize, heighta); | 
 |         updateRectShape(controller, cover, 'n', xa, ya, widtha, handleSize); | 
 |         updateRectShape(controller, cover, 's', xa, y2a, widtha, handleSize); | 
 |  | 
 |         updateRectShape(controller, cover, 'nw', xa, ya, handleSize, handleSize); | 
 |         updateRectShape(controller, cover, 'ne', x2a, ya, handleSize, handleSize); | 
 |         updateRectShape(controller, cover, 'sw', xa, y2a, handleSize, handleSize); | 
 |         updateRectShape(controller, cover, 'se', x2a, y2a, handleSize, handleSize); | 
 |     } | 
 | } | 
 |  | 
 | function updateCommon(controller: BrushController, cover: BrushCover): void { | 
 |     const brushOption = cover.__brushOption; | 
 |     const transformable = brushOption.transformable; | 
 |  | 
 |     const mainEl = cover.childAt(0) as Displayable; | 
 |     mainEl.useStyle(makeStyle(brushOption)); | 
 |     mainEl.attr({ | 
 |         silent: !transformable, | 
 |         cursor: transformable ? 'move' : 'default' | 
 |     }); | 
 |  | 
 |     each( | 
 |         [['w'], ['e'], ['n'], ['s'], ['s', 'e'], ['s', 'w'], ['n', 'e'], ['n', 'w']], | 
 |         function (nameSequence: DirectionNameSequence) { | 
 |             const el = cover.childOfName(nameSequence.join('')) as Displayable; | 
 |             const globalDir = nameSequence.length === 1 | 
 |                 ? getGlobalDirection1(controller, nameSequence[0]) | 
 |                 : getGlobalDirection2(controller, nameSequence); | 
 |  | 
 |             el && el.attr({ | 
 |                 silent: !transformable, | 
 |                 invisible: !transformable, | 
 |                 cursor: transformable ? CURSOR_MAP[globalDir] + '-resize' : null | 
 |             }); | 
 |         } | 
 |     ); | 
 | } | 
 |  | 
 | function updateRectShape( | 
 |     controller: BrushController, | 
 |     cover: BrushCover, | 
 |     name: string, | 
 |     x: number, y: number, w: number, h: number | 
 | ): void { | 
 |     const el = cover.childOfName(name) as graphic.Rect; | 
 |     el && el.setShape(pointsToRect( | 
 |         clipByPanel(controller, cover, [[x, y], [x + w, y + h]]) | 
 |     )); | 
 | } | 
 |  | 
 | function makeStyle(brushOption: BrushCoverConfig) { | 
 |     return defaults({strokeNoScale: true}, brushOption.brushStyle); | 
 | } | 
 |  | 
 | function formatRectRange(x: number, y: number, x2: number, y2: number): BrushDimensionMinMax[] { | 
 |     const min = [mathMin(x, x2), mathMin(y, y2)]; | 
 |     const max = [mathMax(x, x2), mathMax(y, y2)]; | 
 |  | 
 |     return [ | 
 |         [min[0], max[0]], // x range | 
 |         [min[1], max[1]] // y range | 
 |     ]; | 
 | } | 
 |  | 
 | function getTransform(controller: BrushController): matrix.MatrixArray { | 
 |     return graphic.getTransform(controller.group); | 
 | } | 
 |  | 
 | function getGlobalDirection1( | 
 |     controller: BrushController, localDirName: DirectionName | 
 | ): keyof typeof CURSOR_MAP { | 
 |     const map = {w: 'left', e: 'right', n: 'top', s: 'bottom'} as const; | 
 |     const inverseMap = {left: 'w', right: 'e', top: 'n', bottom: 's'} as const; | 
 |     const dir = graphic.transformDirection( | 
 |         map[localDirName], getTransform(controller) | 
 |     ); | 
 |     return inverseMap[dir]; | 
 | } | 
 | function getGlobalDirection2( | 
 |     controller: BrushController, localDirNameSeq: DirectionNameSequence | 
 | ): keyof typeof CURSOR_MAP { | 
 |     const globalDir = [ | 
 |         getGlobalDirection1(controller, localDirNameSeq[0]), | 
 |         getGlobalDirection1(controller, localDirNameSeq[1]) | 
 |     ]; | 
 |     (globalDir[0] === 'e' || globalDir[0] === 'w') && globalDir.reverse(); | 
 |     return globalDir.join('') as keyof typeof CURSOR_MAP; | 
 | } | 
 |  | 
 | function driftRect( | 
 |     rectRangeConverter: RectRangeConverter, | 
 |     controller: BrushController, | 
 |     cover: BrushCover, | 
 |     dirNameSequence: DirectionNameSequence, | 
 |     dx: number, | 
 |     dy: number | 
 | ): void { | 
 |     const brushOption = cover.__brushOption; | 
 |     const rectRange = rectRangeConverter.toRectRange(brushOption.range); | 
 |     const localDelta = toLocalDelta(controller, dx, dy); | 
 |  | 
 |     each(dirNameSequence, function (dirName) { | 
 |         const ind = DIRECTION_MAP[dirName]; | 
 |         rectRange[ind[0]][ind[1]] += localDelta[ind[0]]; | 
 |     }); | 
 |  | 
 |     brushOption.range = rectRangeConverter.fromRectRange(formatRectRange( | 
 |         rectRange[0][0], rectRange[1][0], rectRange[0][1], rectRange[1][1] | 
 |     )); | 
 |  | 
 |     updateCoverAfterCreation(controller, cover); | 
 |     trigger(controller, {isEnd: false}); | 
 | } | 
 |  | 
 | function driftPolygon( | 
 |     controller: BrushController, | 
 |     cover: BrushCover, | 
 |     dx: number, | 
 |     dy: number | 
 | ): void { | 
 |     const range = cover.__brushOption.range as BrushDimensionMinMax[]; | 
 |     const localDelta = toLocalDelta(controller, dx, dy); | 
 |  | 
 |     each(range, function (point) { | 
 |         point[0] += localDelta[0]; | 
 |         point[1] += localDelta[1]; | 
 |     }); | 
 |  | 
 |     updateCoverAfterCreation(controller, cover); | 
 |     trigger(controller, {isEnd: false}); | 
 | } | 
 |  | 
 | function toLocalDelta( | 
 |     controller: BrushController, dx: number, dy: number | 
 | ): BrushDimensionMinMax { | 
 |     const thisGroup = controller.group; | 
 |     const localD = thisGroup.transformCoordToLocal(dx, dy); | 
 |     const localZero = thisGroup.transformCoordToLocal(0, 0); | 
 |  | 
 |     return [localD[0] - localZero[0], localD[1] - localZero[1]]; | 
 | } | 
 |  | 
 | function clipByPanel(controller: BrushController, cover: BrushCover, data: Point[]): Point[] { | 
 |     const panel = getPanelByCover(controller, cover); | 
 |  | 
 |     return (panel && panel !== BRUSH_PANEL_GLOBAL) | 
 |         ? panel.clipPath(data, controller._transform) | 
 |         : clone(data); | 
 | } | 
 |  | 
 | function pointsToRect(points: Point[]): graphic.Rect['shape'] { | 
 |     const xmin = mathMin(points[0][0], points[1][0]); | 
 |     const ymin = mathMin(points[0][1], points[1][1]); | 
 |     const xmax = mathMax(points[0][0], points[1][0]); | 
 |     const ymax = mathMax(points[0][1], points[1][1]); | 
 |  | 
 |     return { | 
 |         x: xmin, | 
 |         y: ymin, | 
 |         width: xmax - xmin, | 
 |         height: ymax - ymin | 
 |     }; | 
 | } | 
 |  | 
 | function resetCursor( | 
 |     controller: BrushController, e: ElementEvent, localCursorPoint: Point | 
 | ): void { | 
 |     if ( | 
 |         // Check active | 
 |         !controller._brushType | 
 |         // resetCursor should be always called when mouse is in zr area, | 
 |         // but not called when mouse is out of zr area to avoid bad influence | 
 |         // if `mousemove`, `mouseup` are triggered from `document` event. | 
 |         || isOutsideZrArea(controller, e.offsetX, e.offsetY) | 
 |     ) { | 
 |         return; | 
 |     } | 
 |  | 
 |     const zr = controller._zr; | 
 |     const covers = controller._covers; | 
 |     const currPanel = getPanelByPoint(controller, e, localCursorPoint); | 
 |  | 
 |     // Check whether in covers. | 
 |     if (!controller._dragging) { | 
 |         for (let i = 0; i < covers.length; i++) { | 
 |             const brushOption = covers[i].__brushOption; | 
 |             if (currPanel | 
 |                 && (currPanel === BRUSH_PANEL_GLOBAL || brushOption.panelId === currPanel.panelId) | 
 |                 && coverRenderers[brushOption.brushType].contain( | 
 |                     covers[i], localCursorPoint[0], localCursorPoint[1] | 
 |                 ) | 
 |             ) { | 
 |                 // Use cursor style set on cover. | 
 |                 return; | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     currPanel && zr.setCursorStyle('crosshair'); | 
 | } | 
 |  | 
 | function preventDefault(e: ElementEvent): void { | 
 |     const rawE = e.event; | 
 |     rawE.preventDefault && rawE.preventDefault(); | 
 | } | 
 |  | 
 | function mainShapeContain(cover: BrushCover, x: number, y: number): boolean { | 
 |     return (cover.childOfName('main') as Displayable).contain(x, y); | 
 | } | 
 |  | 
 | function updateCoverByMouse( | 
 |     controller: BrushController, | 
 |     e: ElementEvent, | 
 |     localCursorPoint: Point, | 
 |     isEnd: boolean | 
 | ): { | 
 |     isEnd: boolean, | 
 |     removeOnClick?: boolean | 
 | } { | 
 |     let creatingCover = controller._creatingCover; | 
 |     const panel = controller._creatingPanel; | 
 |     const thisBrushOption = controller._brushOption; | 
 |     let eventParams; | 
 |  | 
 |     controller._track.push(localCursorPoint.slice()); | 
 |  | 
 |     if (shouldShowCover(controller) || creatingCover) { | 
 |  | 
 |         if (panel && !creatingCover) { | 
 |             thisBrushOption.brushMode === 'single' && clearCovers(controller); | 
 |             const brushOption = clone(thisBrushOption) as BrushCoverConfig; | 
 |             brushOption.brushType = determineBrushType(brushOption.brushType, panel as BrushPanelConfig); | 
 |             brushOption.panelId = panel === BRUSH_PANEL_GLOBAL ? null : panel.panelId; | 
 |             creatingCover = controller._creatingCover = createCover(controller, brushOption); | 
 |             controller._covers.push(creatingCover); | 
 |         } | 
 |  | 
 |         if (creatingCover) { | 
 |             const coverRenderer = coverRenderers[ | 
 |                 determineBrushType(controller._brushType, panel as BrushPanelConfig) | 
 |             ]; | 
 |             const coverBrushOption = creatingCover.__brushOption; | 
 |  | 
 |             coverBrushOption.range = coverRenderer.getCreatingRange( | 
 |                 clipByPanel(controller, creatingCover, controller._track) | 
 |             ); | 
 |  | 
 |             if (isEnd) { | 
 |                 endCreating(controller, creatingCover); | 
 |                 coverRenderer.updateCommon(controller, creatingCover); | 
 |             } | 
 |  | 
 |             updateCoverShape(controller, creatingCover); | 
 |  | 
 |             eventParams = {isEnd: isEnd}; | 
 |         } | 
 |     } | 
 |     else if ( | 
 |         isEnd | 
 |         && thisBrushOption.brushMode === 'single' | 
 |         && thisBrushOption.removeOnClick | 
 |     ) { | 
 |         // Help user to remove covers easily, only by a tiny drag, in 'single' mode. | 
 |         // But a single click do not clear covers, because user may have casual | 
 |         // clicks (for example, click on other component and do not expect covers | 
 |         // disappear). | 
 |         // Only some cover removed, trigger action, but not every click trigger action. | 
 |         if (getPanelByPoint(controller, e, localCursorPoint) && clearCovers(controller)) { | 
 |             eventParams = {isEnd: isEnd, removeOnClick: true}; | 
 |         } | 
 |     } | 
 |  | 
 |     return eventParams; | 
 | } | 
 |  | 
 | function determineBrushType(brushType: BrushTypeUncertain, panel: BrushPanelConfig): BrushType { | 
 |     if (brushType === 'auto') { | 
 |         if (__DEV__) { | 
 |             assert( | 
 |                 panel && panel.defaultBrushType, | 
 |                 'MUST have defaultBrushType when brushType is "atuo"' | 
 |             ); | 
 |         } | 
 |         return panel.defaultBrushType; | 
 |     } | 
 |     return brushType as BrushType; | 
 | } | 
 |  | 
 | const pointerHandlers: Dictionary<(this: BrushController, e: ElementEvent) => void> = { | 
 |  | 
 |     mousedown: function (e) { | 
 |         if (this._dragging) { | 
 |             // In case some browser do not support globalOut, | 
 |             // and release mouse out side the browser. | 
 |             handleDragEnd(this, e); | 
 |         } | 
 |         else if (!e.target || !e.target.draggable) { | 
 |  | 
 |             preventDefault(e); | 
 |  | 
 |             const localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY); | 
 |  | 
 |             this._creatingCover = null; | 
 |             const panel = this._creatingPanel = getPanelByPoint(this, e, localCursorPoint); | 
 |  | 
 |             if (panel) { | 
 |                 this._dragging = true; | 
 |                 this._track = [localCursorPoint.slice()]; | 
 |             } | 
 |         } | 
 |     }, | 
 |  | 
 |     mousemove: function (e) { | 
 |         const x = e.offsetX; | 
 |         const y = e.offsetY; | 
 |  | 
 |         const localCursorPoint = this.group.transformCoordToLocal(x, y); | 
 |  | 
 |         resetCursor(this, e, localCursorPoint); | 
 |  | 
 |         if (this._dragging) { | 
 |             preventDefault(e); | 
 |             const eventParams = updateCoverByMouse(this, e, localCursorPoint, false); | 
 |             eventParams && trigger(this, eventParams); | 
 |         } | 
 |     }, | 
 |  | 
 |     mouseup: function (e) { | 
 |         handleDragEnd(this, e); | 
 |     } | 
 | }; | 
 |  | 
 |  | 
 | function handleDragEnd(controller: BrushController, e: ElementEvent) { | 
 |     if (controller._dragging) { | 
 |         preventDefault(e); | 
 |  | 
 |         const x = e.offsetX; | 
 |         const y = e.offsetY; | 
 |  | 
 |         const localCursorPoint = controller.group.transformCoordToLocal(x, y); | 
 |         const eventParams = updateCoverByMouse(controller, e, localCursorPoint, true); | 
 |  | 
 |         controller._dragging = false; | 
 |         controller._track = []; | 
 |         controller._creatingCover = null; | 
 |  | 
 |         // trigger event shoule be at final, after procedure will be nested. | 
 |         eventParams && trigger(controller, eventParams); | 
 |     } | 
 | } | 
 |  | 
 | function isOutsideZrArea(controller: BrushController, x: number, y: number): boolean { | 
 |     const zr = controller._zr; | 
 |     return x < 0 || x > zr.getWidth() || y < 0 || y > zr.getHeight(); | 
 | } | 
 |  | 
 |  | 
 | interface CoverRenderer { | 
 |     createCover(controller: BrushController, brushOption: BrushCoverConfig): BrushCover; | 
 |     getCreatingRange(localTrack: Point[]): BrushAreaRange; | 
 |     updateCoverShape( | 
 |         controller: BrushController, cover: BrushCover, localRange: BrushAreaRange, brushOption: BrushCoverConfig | 
 |     ): void; | 
 |     updateCommon(controller: BrushController, cover: BrushCover): void; | 
 |     contain(cover: BrushCover, x: number, y: number): boolean; | 
 |     endCreating?(controller: BrushController, creatingCover: BrushCover): void; | 
 | } | 
 |  | 
 | /** | 
 |  * key: brushType | 
 |  */ | 
 | const coverRenderers: Record<BrushType, CoverRenderer> = { | 
 |  | 
 |     lineX: getLineRenderer(0), | 
 |  | 
 |     lineY: getLineRenderer(1), | 
 |  | 
 |     rect: { | 
 |         createCover: function (controller, brushOption) { | 
 |             function returnInput(range: BrushDimensionMinMax[]): BrushDimensionMinMax[] { | 
 |                 return range; | 
 |             } | 
 |             return createBaseRectCover( | 
 |                 { | 
 |                     toRectRange: returnInput, | 
 |                     fromRectRange: returnInput | 
 |                 }, | 
 |                 controller, | 
 |                 brushOption, | 
 |                 [['w'], ['e'], ['n'], ['s'], ['s', 'e'], ['s', 'w'], ['n', 'e'], ['n', 'w']] | 
 |             ); | 
 |         }, | 
 |         getCreatingRange: function (localTrack) { | 
 |             const ends = getTrackEnds(localTrack); | 
 |             return formatRectRange(ends[1][0], ends[1][1], ends[0][0], ends[0][1]); | 
 |         }, | 
 |         updateCoverShape: function (controller, cover, localRange: BrushDimensionMinMax[], brushOption) { | 
 |             updateBaseRect(controller, cover, localRange, brushOption); | 
 |         }, | 
 |         updateCommon: updateCommon, | 
 |         contain: mainShapeContain | 
 |     }, | 
 |  | 
 |     polygon: { | 
 |         createCover: function (controller, brushOption) { | 
 |             const cover = new graphic.Group(); | 
 |  | 
 |             // Do not use graphic.Polygon because graphic.Polyline do not close the | 
 |             // border of the shape when drawing, which is a better experience for user. | 
 |             cover.add(new graphic.Polyline({ | 
 |                 name: 'main', | 
 |                 style: makeStyle(brushOption), | 
 |                 silent: true | 
 |             })); | 
 |  | 
 |             return cover as BrushCover; | 
 |         }, | 
 |         getCreatingRange: function (localTrack) { | 
 |             return localTrack; | 
 |         }, | 
 |         endCreating: function (controller, cover) { | 
 |             cover.remove(cover.childAt(0)); | 
 |             // Use graphic.Polygon close the shape. | 
 |             cover.add(new graphic.Polygon({ | 
 |                 name: 'main', | 
 |                 draggable: true, | 
 |                 drift: curry(driftPolygon, controller, cover), | 
 |                 ondragend: curry(trigger, controller, {isEnd: true}) | 
 |             })); | 
 |         }, | 
 |         updateCoverShape: function (controller, cover, localRange: BrushDimensionMinMax[], brushOption) { | 
 |             (cover.childAt(0) as graphic.Polygon).setShape({ | 
 |                 points: clipByPanel(controller, cover, localRange) | 
 |             }); | 
 |         }, | 
 |         updateCommon: updateCommon, | 
 |         contain: mainShapeContain | 
 |     } | 
 | }; | 
 |  | 
 | function getLineRenderer(xyIndex: 0 | 1) { | 
 |     return { | 
 |         createCover: function (controller: BrushController, brushOption: BrushCoverConfig): BrushCover { | 
 |             return createBaseRectCover( | 
 |                 { | 
 |                     toRectRange: function (range: BrushDimensionMinMax): BrushDimensionMinMax[] { | 
 |                         const rectRange = [range, [0, 100]]; | 
 |                         xyIndex && rectRange.reverse(); | 
 |                         return rectRange; | 
 |                     }, | 
 |                     fromRectRange: function (rectRange: BrushDimensionMinMax[]): BrushDimensionMinMax { | 
 |                         return rectRange[xyIndex]; | 
 |                     } | 
 |                 }, | 
 |                 controller, | 
 |                 brushOption, | 
 |                 ([[['w'], ['e']], [['n'], ['s']]] as DirectionNameSequence[][])[xyIndex] | 
 |             ); | 
 |         }, | 
 |         getCreatingRange: function (localTrack: Point[]): BrushDimensionMinMax { | 
 |             const ends = getTrackEnds(localTrack); | 
 |             const min = mathMin(ends[0][xyIndex], ends[1][xyIndex]); | 
 |             const max = mathMax(ends[0][xyIndex], ends[1][xyIndex]); | 
 |  | 
 |             return [min, max]; | 
 |         }, | 
 |         updateCoverShape: function ( | 
 |             controller: BrushController, | 
 |             cover: BrushCover, | 
 |             localRange: BrushDimensionMinMax, | 
 |             brushOption: BrushCoverConfig | 
 |         ): void { | 
 |             let otherExtent; | 
 |             // If brushWidth not specified, fit the panel. | 
 |             const panel = getPanelByCover(controller, cover); | 
 |             if (panel !== BRUSH_PANEL_GLOBAL && panel.getLinearBrushOtherExtent) { | 
 |                 otherExtent = panel.getLinearBrushOtherExtent(xyIndex); | 
 |             } | 
 |             else { | 
 |                 const zr = controller._zr; | 
 |                 otherExtent = [0, [zr.getWidth(), zr.getHeight()][1 - xyIndex]]; | 
 |             } | 
 |             const rectRange = [localRange, otherExtent]; | 
 |             xyIndex && rectRange.reverse(); | 
 |  | 
 |             updateBaseRect(controller, cover, rectRange, brushOption); | 
 |         }, | 
 |         updateCommon: updateCommon, | 
 |         contain: mainShapeContain | 
 |     }; | 
 | } | 
 |  | 
 | export default BrushController; |