|  |  | 
|  | /* | 
|  | * 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 WeakMap from 'zrender/src/core/WeakMap'; | 
|  | import { ImagePatternObject, PatternObject, SVGPatternObject } from 'zrender/src/graphic/Pattern'; | 
|  | import LRU from 'zrender/src/core/LRU'; | 
|  | import {defaults, map, isArray, isString, isNumber} from 'zrender/src/core/util'; | 
|  | import {getLeastCommonMultiple} from './number'; | 
|  | import {createSymbol} from './symbol'; | 
|  | import ExtensionAPI from '../core/ExtensionAPI'; | 
|  | import type SVGPainter from 'zrender/src/svg/Painter'; | 
|  | import { brushSingle } from 'zrender/src/canvas/graphic'; | 
|  | import {DecalDashArrayX, DecalDashArrayY, InnerDecalObject, DecalObject} from './types'; | 
|  | import { SVGVNode } from 'zrender/src/svg/core'; | 
|  | import { platformApi } from 'zrender/src/core/platform'; | 
|  |  | 
|  | const decalMap = new WeakMap<DecalObject, PatternObject>(); | 
|  |  | 
|  | const decalCache = new LRU<HTMLCanvasElement | SVGVNode>(100); | 
|  |  | 
|  | const decalKeys = [ | 
|  | 'symbol', 'symbolSize', 'symbolKeepAspect', | 
|  | 'color', 'backgroundColor', | 
|  | 'dashArrayX', 'dashArrayY', | 
|  | 'maxTileWidth', 'maxTileHeight' | 
|  | ]; | 
|  |  | 
|  | /** | 
|  | * Create or update pattern image from decal options | 
|  | * | 
|  | * @param {InnerDecalObject | 'none'} decalObject decal options, 'none' if no decal | 
|  | * @return {Pattern} pattern with generated image, null if no decal | 
|  | */ | 
|  | export function createOrUpdatePatternFromDecal( | 
|  | decalObject: InnerDecalObject | 'none', | 
|  | api: ExtensionAPI | 
|  | ): PatternObject { | 
|  | if (decalObject === 'none') { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | const dpr = api.getDevicePixelRatio(); | 
|  | const zr = api.getZr(); | 
|  | const isSVG = zr.painter.type === 'svg'; | 
|  |  | 
|  | if (decalObject.dirty) { | 
|  | decalMap.delete(decalObject); | 
|  | } | 
|  |  | 
|  | const oldPattern = decalMap.get(decalObject); | 
|  | if (oldPattern) { | 
|  | return oldPattern; | 
|  | } | 
|  |  | 
|  | const decalOpt = defaults(decalObject, { | 
|  | symbol: 'rect', | 
|  | symbolSize: 1, | 
|  | symbolKeepAspect: true, | 
|  | color: 'rgba(0, 0, 0, 0.2)', | 
|  | backgroundColor: null, | 
|  | dashArrayX: 5, | 
|  | dashArrayY: 5, | 
|  | rotation: 0, | 
|  | maxTileWidth: 512, | 
|  | maxTileHeight: 512 | 
|  | } as DecalObject); | 
|  | if (decalOpt.backgroundColor === 'none') { | 
|  | decalOpt.backgroundColor = null; | 
|  | } | 
|  |  | 
|  | const pattern: PatternObject = { repeat: 'repeat' } as PatternObject; | 
|  | setPatternnSource(pattern); | 
|  | pattern.rotation = decalOpt.rotation; | 
|  | pattern.scaleX = pattern.scaleY = isSVG ? 1 : 1 / dpr; | 
|  |  | 
|  | decalMap.set(decalObject, pattern); | 
|  |  | 
|  | decalObject.dirty = false; | 
|  |  | 
|  | return pattern; | 
|  |  | 
|  | function setPatternnSource(pattern: PatternObject) { | 
|  | const keys = [dpr]; | 
|  | let isValidKey = true; | 
|  | for (let i = 0; i < decalKeys.length; ++i) { | 
|  | const value = (decalOpt as any)[decalKeys[i]]; | 
|  | if (value != null | 
|  | && !isArray(value) | 
|  | && !isString(value) | 
|  | && !isNumber(value) | 
|  | && typeof value !== 'boolean' | 
|  | ) { | 
|  | isValidKey = false; | 
|  | break; | 
|  | } | 
|  | keys.push(value); | 
|  | } | 
|  |  | 
|  | let cacheKey; | 
|  | if (isValidKey) { | 
|  | cacheKey = keys.join(',') + (isSVG ? '-svg' : ''); | 
|  | const cache = decalCache.get(cacheKey); | 
|  | if (cache) { | 
|  | isSVG ? (pattern as SVGPatternObject).svgElement = cache as SVGVNode | 
|  | : (pattern as ImagePatternObject).image = cache as HTMLCanvasElement; | 
|  | } | 
|  | } | 
|  |  | 
|  | const dashArrayX = normalizeDashArrayX(decalOpt.dashArrayX); | 
|  | const dashArrayY = normalizeDashArrayY(decalOpt.dashArrayY); | 
|  | const symbolArray = normalizeSymbolArray(decalOpt.symbol); | 
|  | const lineBlockLengthsX = getLineBlockLengthX(dashArrayX); | 
|  | const lineBlockLengthY = getLineBlockLengthY(dashArrayY); | 
|  |  | 
|  | const canvas = !isSVG && platformApi.createCanvas(); | 
|  | const svgRoot: SVGVNode = isSVG && { | 
|  | tag: 'g', | 
|  | attrs: {}, | 
|  | key: 'dcl', | 
|  | children: [] | 
|  | }; | 
|  | const pSize = getPatternSize(); | 
|  | let ctx: CanvasRenderingContext2D; | 
|  | if (canvas) { | 
|  | canvas.width = pSize.width * dpr; | 
|  | canvas.height = pSize.height * dpr; | 
|  | ctx = canvas.getContext('2d'); | 
|  | } | 
|  | brushDecal(); | 
|  |  | 
|  | if (isValidKey) { | 
|  | decalCache.put(cacheKey, canvas || svgRoot); | 
|  | } | 
|  |  | 
|  | (pattern as ImagePatternObject).image = canvas; | 
|  | (pattern as SVGPatternObject).svgElement = svgRoot; | 
|  | (pattern as SVGPatternObject).svgWidth = pSize.width; | 
|  | (pattern as SVGPatternObject).svgHeight = pSize.height; | 
|  |  | 
|  | /** | 
|  | * Get minimum length that can make a repeatable pattern. | 
|  | * | 
|  | * @return {Object} pattern width and height | 
|  | */ | 
|  | function getPatternSize(): { | 
|  | width: number, | 
|  | height: number | 
|  | } { | 
|  | /** | 
|  | * For example, if dash is [[3, 2], [2, 1]] for X, it looks like | 
|  | * |---  ---  ---  ---  --- ... | 
|  | * |-- -- -- -- -- -- -- -- ... | 
|  | * |---  ---  ---  ---  --- ... | 
|  | * |-- -- -- -- -- -- -- -- ... | 
|  | * So the minimum length of X is 15, | 
|  | * which is the least common multiple of `3 + 2` and `2 + 1` | 
|  | * |---  ---  ---  |---  --- ... | 
|  | * |-- -- -- -- -- |-- -- -- ... | 
|  | */ | 
|  | let width = 1; | 
|  | for (let i = 0, xlen = lineBlockLengthsX.length; i < xlen; ++i) { | 
|  | width = getLeastCommonMultiple(width, lineBlockLengthsX[i]); | 
|  | } | 
|  |  | 
|  | let symbolRepeats = 1; | 
|  | for (let i = 0, xlen = symbolArray.length; i < xlen; ++i) { | 
|  | symbolRepeats = getLeastCommonMultiple(symbolRepeats, symbolArray[i].length); | 
|  | } | 
|  | width *= symbolRepeats; | 
|  |  | 
|  | const height = lineBlockLengthY * lineBlockLengthsX.length * symbolArray.length; | 
|  |  | 
|  | if (__DEV__) { | 
|  | const warn = (attrName: string) => { | 
|  | /* eslint-disable-next-line */ | 
|  | console.warn(`Calculated decal size is greater than ${attrName} due to decal option settings so ${attrName} is used for the decal size. Please consider changing the decal option to make a smaller decal or set ${attrName} to be larger to avoid incontinuity.`); | 
|  | }; | 
|  | if (width > decalOpt.maxTileWidth) { | 
|  | warn('maxTileWidth'); | 
|  | } | 
|  | if (height > decalOpt.maxTileHeight) { | 
|  | warn('maxTileHeight'); | 
|  | } | 
|  | } | 
|  |  | 
|  | return { | 
|  | width: Math.max(1, Math.min(width, decalOpt.maxTileWidth)), | 
|  | height: Math.max(1, Math.min(height, decalOpt.maxTileHeight)) | 
|  | }; | 
|  | } | 
|  |  | 
|  | function brushDecal() { | 
|  | if (ctx) { | 
|  | ctx.clearRect(0, 0, canvas.width, canvas.height); | 
|  | if (decalOpt.backgroundColor) { | 
|  | ctx.fillStyle = decalOpt.backgroundColor; | 
|  | ctx.fillRect(0, 0, canvas.width, canvas.height); | 
|  | } | 
|  | } | 
|  |  | 
|  | let ySum = 0; | 
|  | for (let i = 0; i < dashArrayY.length; ++i) { | 
|  | ySum += dashArrayY[i]; | 
|  | } | 
|  | if (ySum <= 0) { | 
|  | // dashArrayY is 0, draw nothing | 
|  | return; | 
|  | } | 
|  |  | 
|  | let y = -lineBlockLengthY; | 
|  | let yId = 0; | 
|  | let yIdTotal = 0; | 
|  | let xId0 = 0; | 
|  | while (y < pSize.height) { | 
|  | if (yId % 2 === 0) { | 
|  | const symbolYId = (yIdTotal / 2) % symbolArray.length; | 
|  | let x = 0; | 
|  | let xId1 = 0; | 
|  | let xId1Total = 0; | 
|  | while (x < pSize.width * 2) { | 
|  | let xSum = 0; | 
|  | for (let i = 0; i < dashArrayX[xId0].length; ++i) { | 
|  | xSum += dashArrayX[xId0][i]; | 
|  | } | 
|  | if (xSum <= 0) { | 
|  | // Skip empty line | 
|  | break; | 
|  | } | 
|  |  | 
|  | // E.g., [15, 5, 20, 5] draws only for 15 and 20 | 
|  | if (xId1 % 2 === 0) { | 
|  | const size = (1 - decalOpt.symbolSize) * 0.5; | 
|  | const left = x + dashArrayX[xId0][xId1] * size; | 
|  | const top = y + dashArrayY[yId] * size; | 
|  | const width = dashArrayX[xId0][xId1] * decalOpt.symbolSize; | 
|  | const height = dashArrayY[yId] * decalOpt.symbolSize; | 
|  | const symbolXId = (xId1Total / 2) % symbolArray[symbolYId].length; | 
|  |  | 
|  | brushSymbol(left, top, width, height, symbolArray[symbolYId][symbolXId]); | 
|  | } | 
|  |  | 
|  | x += dashArrayX[xId0][xId1]; | 
|  | ++xId1Total; | 
|  | ++xId1; | 
|  | if (xId1 === dashArrayX[xId0].length) { | 
|  | xId1 = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | ++xId0; | 
|  | if (xId0 === dashArrayX.length) { | 
|  | xId0 = 0; | 
|  | } | 
|  | } | 
|  | y += dashArrayY[yId]; | 
|  |  | 
|  | ++yIdTotal; | 
|  | ++yId; | 
|  | if (yId === dashArrayY.length) { | 
|  | yId = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | function brushSymbol(x: number, y: number, width: number, height: number, symbolType: string) { | 
|  | const scale = isSVG ? 1 : dpr; | 
|  | const symbol = createSymbol( | 
|  | symbolType, | 
|  | x * scale, | 
|  | y * scale, | 
|  | width * scale, | 
|  | height * scale, | 
|  | decalOpt.color, | 
|  | decalOpt.symbolKeepAspect | 
|  | ); | 
|  | if (isSVG) { | 
|  | const symbolVNode = (zr.painter as SVGPainter).renderOneToVNode(symbol); | 
|  | if (symbolVNode) { | 
|  | svgRoot.children.push(symbolVNode); | 
|  | } | 
|  | } | 
|  | else { | 
|  | // Paint to canvas for all other renderers. | 
|  | brushSingle(ctx, symbol); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Convert symbol array into normalized array | 
|  | * | 
|  | * @param {string | (string | string[])[]} symbol symbol input | 
|  | * @return {string[][]} normolized symbol array | 
|  | */ | 
|  | function normalizeSymbolArray(symbol: string | (string | string[])[]): string[][] { | 
|  | if (!symbol || (symbol as string[]).length === 0) { | 
|  | return [['rect']]; | 
|  | } | 
|  | if (isString(symbol)) { | 
|  | return [[symbol]]; | 
|  | } | 
|  |  | 
|  | let isAllString = true; | 
|  | for (let i = 0; i < symbol.length; ++i) { | 
|  | if (!isString(symbol[i])) { | 
|  | isAllString = false; | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (isAllString) { | 
|  | return normalizeSymbolArray([symbol as string[]]); | 
|  | } | 
|  |  | 
|  | const result: string[][] = []; | 
|  | for (let i = 0; i < symbol.length; ++i) { | 
|  | if (isString(symbol[i])) { | 
|  | result.push([symbol[i] as string]); | 
|  | } | 
|  | else { | 
|  | result.push(symbol[i] as string[]); | 
|  | } | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Convert dash input into dashArray | 
|  | * | 
|  | * @param {DecalDashArrayX} dash dash input | 
|  | * @return {number[][]} normolized dash array | 
|  | */ | 
|  | function normalizeDashArrayX(dash: DecalDashArrayX): number[][] { | 
|  | if (!dash || (dash as number[]).length === 0) { | 
|  | return [[0, 0]]; | 
|  | } | 
|  | if (isNumber(dash)) { | 
|  | const dashValue = Math.ceil(dash); | 
|  | return [[dashValue, dashValue]]; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * [20, 5] should be normalized into [[20, 5]], | 
|  | * while [20, [5, 10]] should be normalized into [[20, 20], [5, 10]] | 
|  | */ | 
|  | let isAllNumber = true; | 
|  | for (let i = 0; i < dash.length; ++i) { | 
|  | if (!isNumber(dash[i])) { | 
|  | isAllNumber = false; | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (isAllNumber) { | 
|  | return normalizeDashArrayX([dash as number[]]); | 
|  | } | 
|  |  | 
|  | const result: number[][] = []; | 
|  | for (let i = 0; i < dash.length; ++i) { | 
|  | if (isNumber(dash[i])) { | 
|  | const dashValue = Math.ceil(dash[i] as number); | 
|  | result.push([dashValue, dashValue]); | 
|  | } | 
|  | else { | 
|  | const dashValue = map(dash[i] as number[], n => Math.ceil(n)); | 
|  | if (dashValue.length % 2 === 1) { | 
|  | // [4, 2, 1] means |----  -    -- |----  -    -- | | 
|  | // so normalize it to be [4, 2, 1, 4, 2, 1] | 
|  | result.push(dashValue.concat(dashValue)); | 
|  | } | 
|  | else { | 
|  | result.push(dashValue); | 
|  | } | 
|  | } | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Convert dash input into dashArray | 
|  | * | 
|  | * @param {DecalDashArrayY} dash dash input | 
|  | * @return {number[]} normolized dash array | 
|  | */ | 
|  | function normalizeDashArrayY(dash: DecalDashArrayY): number[] { | 
|  | if (!dash || typeof dash === 'object' && dash.length === 0) { | 
|  | return [0, 0]; | 
|  | } | 
|  | if (isNumber(dash)) { | 
|  | const dashValue = Math.ceil(dash); | 
|  | return [dashValue, dashValue]; | 
|  | } | 
|  |  | 
|  | const dashValue = map(dash as number[], n => Math.ceil(n)); | 
|  | return dash.length % 2 ? dashValue.concat(dashValue) : dashValue; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get block length of each line. A block is the length of dash line and space. | 
|  | * For example, a line with [4, 1] has a dash line of 4 and a space of 1 after | 
|  | * that, so the block length of this line is 5. | 
|  | * | 
|  | * @param {number[][]} dash dash array of X or Y | 
|  | * @return {number[]} block length of each line | 
|  | */ | 
|  | function getLineBlockLengthX(dash: number[][]): number[] { | 
|  | return map(dash, function (line) { | 
|  | return getLineBlockLengthY(line); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function getLineBlockLengthY(dash: number[]): number { | 
|  | let blockLength = 0; | 
|  | for (let i = 0; i < dash.length; ++i) { | 
|  | blockLength += dash[i]; | 
|  | } | 
|  | if (dash.length % 2 === 1) { | 
|  | // [4, 2, 1] means |----  -    -- |----  -    -- | | 
|  | // So total length is (4 + 2 + 1) * 2 | 
|  | return blockLength * 2; | 
|  | } | 
|  | return blockLength; | 
|  | } |