|  | /* | 
|  | * 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. | 
|  | */ | 
|  |  | 
|  | /* global Float32Array */ | 
|  |  | 
|  | // TODO Batch by color | 
|  |  | 
|  | import * as graphic from '../../util/graphic'; | 
|  | import {createSymbol} from '../../util/symbol'; | 
|  | import SeriesData from '../../data/SeriesData'; | 
|  | import { PathProps } from 'zrender/src/graphic/Path'; | 
|  | import PathProxy from 'zrender/src/core/PathProxy'; | 
|  | import SeriesModel from '../../model/Series'; | 
|  | import { StageHandlerProgressParams } from '../../util/types'; | 
|  | import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; | 
|  | import { getECData } from '../../util/innerStore'; | 
|  | import Element from 'zrender/src/Element'; | 
|  |  | 
|  | const BOOST_SIZE_THRESHOLD = 4; | 
|  |  | 
|  | class LargeSymbolPathShape { | 
|  | points: ArrayLike<number>; | 
|  | size: number[]; | 
|  | } | 
|  |  | 
|  | type LargeSymbolPathProps = PathProps & { | 
|  | shape?: Partial<LargeSymbolPathShape> | 
|  | startIndex?: number | 
|  | endIndex?: number | 
|  | }; | 
|  |  | 
|  | type ECSymbol = ReturnType<typeof createSymbol>; | 
|  |  | 
|  | class LargeSymbolPath extends graphic.Path<LargeSymbolPathProps> { | 
|  |  | 
|  | shape: LargeSymbolPathShape; | 
|  |  | 
|  | symbolProxy: ECSymbol; | 
|  |  | 
|  | softClipShape: CoordinateSystemClipArea; | 
|  |  | 
|  | startIndex: number; | 
|  | endIndex: number; | 
|  |  | 
|  | private _ctx: CanvasRenderingContext2D; | 
|  | private _off: number = 0; | 
|  |  | 
|  | hoverDataIdx: number = -1; | 
|  |  | 
|  | notClear: boolean; | 
|  |  | 
|  | constructor(opts?: LargeSymbolPathProps) { | 
|  | super(opts); | 
|  | } | 
|  |  | 
|  | getDefaultShape() { | 
|  | return new LargeSymbolPathShape(); | 
|  | } | 
|  |  | 
|  | setColor: ECSymbol['setColor']; | 
|  |  | 
|  | reset() { | 
|  | this.notClear = false; | 
|  | this._off = 0; | 
|  | } | 
|  |  | 
|  | buildPath(path: PathProxy | CanvasRenderingContext2D, shape: LargeSymbolPathShape) { | 
|  | const points = shape.points; | 
|  | const size = shape.size; | 
|  |  | 
|  | const symbolProxy = this.symbolProxy; | 
|  | const symbolProxyShape = symbolProxy.shape; | 
|  | const ctx = (path as PathProxy).getContext | 
|  | ? (path as PathProxy).getContext() | 
|  | : path as CanvasRenderingContext2D; | 
|  | const canBoost = ctx && size[0] < BOOST_SIZE_THRESHOLD; | 
|  | const softClipShape = this.softClipShape; | 
|  | let i; | 
|  |  | 
|  | // Do draw in afterBrush. | 
|  | if (canBoost) { | 
|  | this._ctx = ctx; | 
|  | return; | 
|  | } | 
|  |  | 
|  | this._ctx = null; | 
|  |  | 
|  | for (i = this._off; i < points.length;) { | 
|  | const x = points[i++]; | 
|  | const y = points[i++]; | 
|  |  | 
|  | if (isNaN(x) || isNaN(y)) { | 
|  | continue; | 
|  | } | 
|  | if (softClipShape && !softClipShape.contain(x, y)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | symbolProxyShape.x = x - size[0] / 2; | 
|  | symbolProxyShape.y = y - size[1] / 2; | 
|  | symbolProxyShape.width = size[0]; | 
|  | symbolProxyShape.height = size[1]; | 
|  |  | 
|  | symbolProxy.buildPath(path, symbolProxyShape, true); | 
|  | } | 
|  | if (this.incremental) { | 
|  | this._off = i; | 
|  | this.notClear = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | afterBrush() { | 
|  | const shape = this.shape; | 
|  | const points = shape.points; | 
|  | const size = shape.size; | 
|  | const ctx = this._ctx; | 
|  | const softClipShape = this.softClipShape; | 
|  | let i; | 
|  |  | 
|  | if (!ctx) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // PENDING If style or other canvas status changed? | 
|  | for (i = this._off; i < points.length;) { | 
|  | const x = points[i++]; | 
|  | const y = points[i++]; | 
|  | if (isNaN(x) || isNaN(y)) { | 
|  | continue; | 
|  | } | 
|  | if (softClipShape && !softClipShape.contain(x, y)) { | 
|  | continue; | 
|  | } | 
|  | // fillRect is faster than building a rect path and draw. | 
|  | // And it support light globalCompositeOperation. | 
|  | ctx.fillRect( | 
|  | x - size[0] / 2, y - size[1] / 2, | 
|  | size[0], size[1] | 
|  | ); | 
|  | } | 
|  | if (this.incremental) { | 
|  | this._off = i; | 
|  | this.notClear = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | findDataIndex(x: number, y: number) { | 
|  | // TODO ??? | 
|  | // Consider transform | 
|  |  | 
|  | const shape = this.shape; | 
|  | const points = shape.points; | 
|  | const size = shape.size; | 
|  |  | 
|  | const w = Math.max(size[0], 4); | 
|  | const h = Math.max(size[1], 4); | 
|  |  | 
|  | // Not consider transform | 
|  | // Treat each element as a rect | 
|  | // top down traverse | 
|  | for (let idx = points.length / 2 - 1; idx >= 0; idx--) { | 
|  | const i = idx * 2; | 
|  | const x0 = points[i] - w / 2; | 
|  | const y0 = points[i + 1] - h / 2; | 
|  | if (x >= x0 && y >= y0 && x <= x0 + w && y <= y0 + h) { | 
|  | return idx; | 
|  | } | 
|  | } | 
|  |  | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | contain(x: number, y: number): boolean { | 
|  | const localPos = this.transformCoordToLocal(x, y); | 
|  | const rect = this.getBoundingRect(); | 
|  | x = localPos[0]; | 
|  | y = localPos[1]; | 
|  |  | 
|  | if (rect.contain(x, y)) { | 
|  | // Cache found data index. | 
|  | const dataIdx = this.hoverDataIdx = this.findDataIndex(x, y); | 
|  | return dataIdx >= 0; | 
|  | } | 
|  | this.hoverDataIdx = -1; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | getBoundingRect() { | 
|  | // Ignore stroke for large symbol draw. | 
|  | let rect = this._rect; | 
|  | if (!rect) { | 
|  | const shape = this.shape; | 
|  | const points = shape.points; | 
|  | const size = shape.size; | 
|  | const w = size[0]; | 
|  | const h = size[1]; | 
|  | let minX = Infinity; | 
|  | let minY = Infinity; | 
|  | let maxX = -Infinity; | 
|  | let maxY = -Infinity; | 
|  | for (let i = 0; i < points.length;) { | 
|  | const x = points[i++]; | 
|  | const y = points[i++]; | 
|  | minX = Math.min(x, minX); | 
|  | maxX = Math.max(x, maxX); | 
|  | minY = Math.min(y, minY); | 
|  | maxY = Math.max(y, maxY); | 
|  | } | 
|  |  | 
|  | rect = this._rect = new graphic.BoundingRect( | 
|  | minX - w / 2, | 
|  | minY - h / 2, | 
|  | maxX - minX + w, | 
|  | maxY - minY + h | 
|  | ); | 
|  | } | 
|  | return rect; | 
|  | } | 
|  | } | 
|  |  | 
|  | interface UpdateOpt { | 
|  | clipShape?: CoordinateSystemClipArea | 
|  | } | 
|  |  | 
|  | class LargeSymbolDraw { | 
|  |  | 
|  | group = new graphic.Group(); | 
|  |  | 
|  | // New add element in this frame of progressive render. | 
|  | private _newAdded: LargeSymbolPath[]; | 
|  |  | 
|  | /** | 
|  | * Update symbols draw by new data | 
|  | */ | 
|  | updateData(data: SeriesData, opt?: UpdateOpt) { | 
|  | this._clear(); | 
|  |  | 
|  | const symbolEl = this._create(); | 
|  | symbolEl.setShape({ | 
|  | points: data.getLayout('points') | 
|  | }); | 
|  | this._setCommon(symbolEl, data, opt); | 
|  | } | 
|  |  | 
|  | updateLayout(data: SeriesData) { | 
|  | let points = data.getLayout('points'); | 
|  | this.group.eachChild(function (child: LargeSymbolPath) { | 
|  | if (child.startIndex != null) { | 
|  | const len = (child.endIndex - child.startIndex) * 2; | 
|  | const byteOffset = child.startIndex * 4 * 2; | 
|  | points = new Float32Array(points.buffer, byteOffset, len); | 
|  | } | 
|  | child.setShape('points', points); | 
|  | // Reset draw cursor. | 
|  | child.reset(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | incrementalPrepareUpdate(data: SeriesData) { | 
|  | this._clear(); | 
|  | } | 
|  |  | 
|  | incrementalUpdate(taskParams: StageHandlerProgressParams, data: SeriesData, opt: UpdateOpt) { | 
|  | const lastAdded = this._newAdded[0]; | 
|  | const points = data.getLayout('points'); | 
|  | const oldPoints = lastAdded && lastAdded.shape.points; | 
|  | // Merging the exists. Each element has 1e4 points. | 
|  | // Consider the performance balance between too much elements and too much points in one shape(may affect hover optimization) | 
|  | if (oldPoints && oldPoints.length < 2e4) { | 
|  | const oldLen = oldPoints.length; | 
|  | const newPoints = new Float32Array(oldLen + points.length); | 
|  | // Concat two array | 
|  | newPoints.set(oldPoints); | 
|  | newPoints.set(points, oldLen); | 
|  | // Update endIndex | 
|  | lastAdded.endIndex = taskParams.end; | 
|  | lastAdded.setShape({ points: newPoints }); | 
|  | } | 
|  | else { | 
|  | // Clear | 
|  | this._newAdded = []; | 
|  |  | 
|  | const symbolEl = this._create(); | 
|  | symbolEl.startIndex = taskParams.start; | 
|  | symbolEl.endIndex = taskParams.end; | 
|  | symbolEl.incremental = true; | 
|  | symbolEl.setShape({ | 
|  | points | 
|  | }); | 
|  | this._setCommon(symbolEl, data, opt); | 
|  | } | 
|  | } | 
|  |  | 
|  | eachRendered(cb: (el: Element) => boolean | void) { | 
|  | this._newAdded[0] && cb(this._newAdded[0]); | 
|  | } | 
|  |  | 
|  | private _create() { | 
|  | const symbolEl = new LargeSymbolPath({ | 
|  | cursor: 'default' | 
|  | }); | 
|  | symbolEl.ignoreCoarsePointer = true; | 
|  | this.group.add(symbolEl); | 
|  | this._newAdded.push(symbolEl); | 
|  | return symbolEl; | 
|  | } | 
|  |  | 
|  | private _setCommon( | 
|  | symbolEl: LargeSymbolPath, | 
|  | data: SeriesData, | 
|  | opt: UpdateOpt | 
|  | ) { | 
|  | const hostModel = data.hostModel; | 
|  |  | 
|  | opt = opt || {}; | 
|  |  | 
|  | const size = data.getVisual('symbolSize'); | 
|  | symbolEl.setShape('size', (size instanceof Array) ? size : [size, size]); | 
|  |  | 
|  | symbolEl.softClipShape = opt.clipShape || null; | 
|  | // Create symbolProxy to build path for each data | 
|  | symbolEl.symbolProxy = createSymbol( | 
|  | data.getVisual('symbol'), 0, 0, 0, 0 | 
|  | ); | 
|  | // Use symbolProxy setColor method | 
|  | symbolEl.setColor = symbolEl.symbolProxy.setColor; | 
|  |  | 
|  | const extrudeShadow = symbolEl.shape.size[0] < BOOST_SIZE_THRESHOLD; | 
|  | symbolEl.useStyle( | 
|  | // Draw shadow when doing fillRect is extremely slow. | 
|  | hostModel.getModel('itemStyle').getItemStyle( | 
|  | extrudeShadow ? ['color', 'shadowBlur', 'shadowColor'] : ['color'] | 
|  | ) | 
|  | ); | 
|  |  | 
|  | const globalStyle = data.getVisual('style'); | 
|  | const visualColor = globalStyle && globalStyle.fill; | 
|  | if (visualColor) { | 
|  | symbolEl.setColor(visualColor); | 
|  | } | 
|  |  | 
|  | const ecData = getECData(symbolEl); | 
|  | // Enable tooltip | 
|  | // PENDING May have performance issue when path is extremely large | 
|  | ecData.seriesIndex = (hostModel as SeriesModel).seriesIndex; | 
|  | symbolEl.on('mousemove', function (e) { | 
|  | ecData.dataIndex = null; | 
|  | const dataIndex = symbolEl.hoverDataIdx; | 
|  | if (dataIndex >= 0) { | 
|  | // Provide dataIndex for tooltip | 
|  | ecData.dataIndex = dataIndex + (symbolEl.startIndex || 0); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | remove() { | 
|  | this._clear(); | 
|  | } | 
|  |  | 
|  | private _clear() { | 
|  | this._newAdded = []; | 
|  | this.group.removeAll(); | 
|  | } | 
|  | } | 
|  |  | 
|  |  | 
|  | export default LargeSymbolDraw; |