|  | 
 | /* | 
 | * 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; | 
 | } |