| /* | |
| * 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 { parseSVG, makeViewBoxTransform, SVGNodeTagLower, SVGParserResultNamedItem } from 'zrender/src/tool/parseSVG'; | |
| import Group from 'zrender/src/graphic/Group'; | |
| import Rect from 'zrender/src/graphic/shape/Rect'; | |
| import {assert, createHashMap, each, HashMap} from 'zrender/src/core/util'; | |
| import BoundingRect from 'zrender/src/core/BoundingRect'; | |
| import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput } from './geoTypes'; | |
| import { parseXML } from 'zrender/src/tool/parseXML'; | |
| import { GeoSVGRegion } from './Region'; | |
| import Element from 'zrender/src/Element'; | |
| export interface GeoSVGGraphicRecord { | |
| root: Group; | |
| boundingRect: BoundingRect; | |
| named: SVGParserResultNamedItem[]; | |
| } | |
| /** | |
| * "region available" means that: enable users to set attribute `name="xxx"` on those tags | |
| * to make it be a region. | |
| * 1. region styles and its label styles can be defined in echarts opton: | |
| * ```js | |
| * geo: { | |
| * regions: [{ | |
| * name: 'xxx', | |
| * itemStyle: { ... }, | |
| * label: { ... } | |
| * }, { | |
| * ... | |
| * }, | |
| * ...] | |
| * }; | |
| * ``` | |
| * 2. name can be duplicated in different SVG tag. All of the tags with the same name share | |
| * a region option. For exampel if there are two <path> representing two lung lobes. They have | |
| * no common parents but both of them need to display label "lung" inside. | |
| */ | |
| const REGION_AVAILABLE_SVG_TAG_MAP = createHashMap<number, SVGNodeTagLower>([ | |
| 'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path', | |
| // <text> <tspan> are also enabled because some SVG might paint text itself, | |
| // but still need to trigger events or tooltip. | |
| 'text', 'tspan', | |
| // <g> is also enabled because this case: if multiple tags share one name | |
| // and need label displayed, every tags will display the name, which is not | |
| // expected. So we can put them into a <g name="xxx">. Thereby only one label | |
| // displayed and located based on the bounding rect of the <g>. | |
| 'g' | |
| ]); | |
| export class GeoSVGResource implements GeoResource { | |
| readonly type = 'geoSVG'; | |
| private _mapName: string; | |
| private _parsedXML: SVGElement; | |
| private _firstGraphic: GeoSVGGraphicRecord; | |
| private _boundingRect: BoundingRect; | |
| private _regions: GeoSVGRegion[]; | |
| // Key: region.name | |
| private _regionsMap: HashMap<GeoSVGRegion>; | |
| // All used graphics. key: hostKey, value: root | |
| private _usedGraphicMap: HashMap<GeoSVGGraphicRecord> = createHashMap(); | |
| // All unused graphics. | |
| private _freedGraphics: GeoSVGGraphicRecord[] = []; | |
| constructor( | |
| mapName: string, | |
| svg: GeoSVGSourceInput | |
| ) { | |
| this._mapName = mapName; | |
| // Only perform parse to XML object here, which might be time | |
| // consiming for large SVG. | |
| // Although convert XML to zrender element is also time consiming, | |
| // if we do it here, the clone of zrender elements has to be | |
| // required. So we do it once for each geo instance, util real | |
| // performance issues call for optimizing it. | |
| this._parsedXML = parseXML(svg); | |
| } | |
| load(/* nameMap: NameMap */) { | |
| // In the "load" stage, graphic need to be built to | |
| // get boundingRect for geo coordinate system. | |
| let firstGraphic = this._firstGraphic; | |
| // Create the return data structure only when first graphic created. | |
| // Because they will be used in geo coordinate system update stage, | |
| // and `regions` will be mounted at `geo` coordinate system, | |
| // in which there is no "view" info, so that it should better not to | |
| // make references to graphic elements. | |
| if (!firstGraphic) { | |
| firstGraphic = this._firstGraphic = this._buildGraphic(this._parsedXML); | |
| this._freedGraphics.push(firstGraphic); | |
| this._boundingRect = this._firstGraphic.boundingRect.clone(); | |
| // PENDING: `nameMap` will not be supported until some real requirement come. | |
| // if (nameMap) { | |
| // named = applyNameMap(named, nameMap); | |
| // } | |
| const { regions, regionsMap } = createRegions(firstGraphic.named); | |
| this._regions = regions; | |
| this._regionsMap = regionsMap; | |
| } | |
| return { | |
| boundingRect: this._boundingRect, | |
| regions: this._regions, | |
| regionsMap: this._regionsMap | |
| }; | |
| } | |
| private _buildGraphic( | |
| svgXML: SVGElement | |
| ): GeoSVGGraphicRecord { | |
| let result; | |
| let rootFromParse; | |
| try { | |
| result = svgXML && parseSVG(svgXML, { | |
| ignoreViewBox: true, | |
| ignoreRootClip: true | |
| }) || {}; | |
| rootFromParse = result.root; | |
| assert(rootFromParse != null); | |
| } | |
| catch (e) { | |
| throw new Error('Invalid svg format\n' + e.message); | |
| } | |
| // Note: we keep the covenant that the root has no transform. So always add an extra root. | |
| const root = new Group(); | |
| root.add(rootFromParse); | |
| (root as GeoSVGGraphicRoot).isGeoSVGGraphicRoot = true; | |
| // [THE_RULE_OF_VIEWPORT_AND_VIEWBOX] | |
| // | |
| // Consider: `<svg width="..." height="..." viewBox="...">` | |
| // - the `width/height` we call it `svgWidth/svgHeight` for short. | |
| // - `(0, 0, svgWidth, svgHeight)` defines the viewport of the SVG, or say, | |
| // "viewport boundingRect", or `boundingRect` for short. | |
| // - `viewBox` defines the transform from the real content ot the viewport. | |
| // `viewBox` has the same unit as the content of SVG. | |
| // If `viewBox` exists, a transform is defined, so the unit of `svgWidth/svgHeight` become | |
| // different from the content of SVG. Otherwise, they are the same. | |
| // | |
| // If both `svgWidth/svgHeight/viewBox` are specified in a SVG file, the transform rule will be: | |
| // 0. `boundingRect` is `(0, 0, svgWidth, svgHeight)`. Set it to Geo['_rect'] (View['_rect']). | |
| // 1. Make a transform from `viewBox` to `boundingRect`. | |
| // Note: only support `preserveAspectRatio 'xMidYMid'` here. That is, this transform will preserve | |
| // the aspect ratio. | |
| // 2. Make a transform from boundingRect to Geo['_viewRect'] (View['_viewRect']) | |
| // (`Geo`/`View` will do this job). | |
| // Note: this transform might not preserve aspect radio, which depending on how users specify | |
| // viewRect in echarts option (e.g., `geo.left/top/width/height` will not preserve aspect ratio, | |
| // but `geo.layoutCenter/layoutSize` will preserve aspect ratio). | |
| // | |
| // If `svgWidth/svgHeight` not specified, we use `viewBox` as the `boundingRect` to make the SVG | |
| // layout look good. | |
| // | |
| // If neither `svgWidth/svgHeight` nor `viewBox` are not specified, we calculate the boundingRect | |
| // of the SVG content and use them to make SVG layout look good. | |
| const svgWidth = result.width; | |
| const svgHeight = result.height; | |
| const viewBoxRect = result.viewBoxRect; | |
| let boundingRect = this._boundingRect; | |
| if (!boundingRect) { | |
| let bRectX; | |
| let bRectY; | |
| let bRectWidth; | |
| let bRectHeight; | |
| if (svgWidth != null) { | |
| bRectX = 0; | |
| bRectWidth = svgWidth; | |
| } | |
| else if (viewBoxRect) { | |
| bRectX = viewBoxRect.x; | |
| bRectWidth = viewBoxRect.width; | |
| } | |
| if (svgHeight != null) { | |
| bRectY = 0; | |
| bRectHeight = svgHeight; | |
| } | |
| else if (viewBoxRect) { | |
| bRectY = viewBoxRect.y; | |
| bRectHeight = viewBoxRect.height; | |
| } | |
| // If both viewBox and svgWidth/svgHeight not specified, | |
| // we have to determine how to layout those element to make them look good. | |
| if (bRectX == null || bRectY == null) { | |
| const calculatedBoundingRect = rootFromParse.getBoundingRect(); | |
| if (bRectX == null) { | |
| bRectX = calculatedBoundingRect.x; | |
| bRectWidth = calculatedBoundingRect.width; | |
| } | |
| if (bRectY == null) { | |
| bRectY = calculatedBoundingRect.y; | |
| bRectHeight = calculatedBoundingRect.height; | |
| } | |
| } | |
| boundingRect = this._boundingRect = new BoundingRect(bRectX, bRectY, bRectWidth, bRectHeight); | |
| } | |
| if (viewBoxRect) { | |
| const viewBoxTransform = makeViewBoxTransform(viewBoxRect, boundingRect); | |
| // Only support `preserveAspectRatio 'xMidYMid'` | |
| rootFromParse.scaleX = rootFromParse.scaleY = viewBoxTransform.scale; | |
| rootFromParse.x = viewBoxTransform.x; | |
| rootFromParse.y = viewBoxTransform.y; | |
| } | |
| // SVG needs to clip based on `viewBox`. And some SVG files really rely on this feature. | |
| // They do not strictly confine all of the content inside a display rect, but deliberately | |
| // use a `viewBox` to define a displayable rect. | |
| // PENDING: | |
| // The drawback of the `setClipPath` here is: the region label (genereted by echarts) near the | |
| // edge might also be clipped, because region labels are put as `textContent` of the SVG path. | |
| root.setClipPath(new Rect({ | |
| shape: boundingRect.plain() | |
| })); | |
| const named = [] as GeoSVGGraphicRecord['named']; | |
| each(result.named, namedItem => { | |
| if (REGION_AVAILABLE_SVG_TAG_MAP.get(namedItem.svgNodeTagLower) != null) { | |
| named.push(namedItem); | |
| setSilent(namedItem.el); | |
| } | |
| }); | |
| return { root, boundingRect, named }; | |
| } | |
| /** | |
| * Consider: | |
| * (1) One graphic element can not be shared by different `geoView` running simultaneously. | |
| * Notice, also need to consider multiple echarts instances share a `mapRecord`. | |
| * (2) Converting SVG to graphic elements is time consuming. | |
| * (3) In the current architecture, `load` should be called frequently to get boundingRect, | |
| * and it is called without view info. | |
| * So we maintain graphic elements in this module, and enables `view` to use/return these | |
| * graphics from/to the pool with it's uid. | |
| */ | |
| useGraphic(hostKey: string /* , nameMap: NameMap */): GeoSVGGraphicRecord { | |
| const usedRootMap = this._usedGraphicMap; | |
| let svgGraphic = usedRootMap.get(hostKey); | |
| if (svgGraphic) { | |
| return svgGraphic; | |
| } | |
| svgGraphic = this._freedGraphics.pop() | |
| // use the first boundingRect to avoid duplicated boundingRect calculation. | |
| || this._buildGraphic(this._parsedXML); | |
| usedRootMap.set(hostKey, svgGraphic); | |
| // PENDING: `nameMap` will not be supported until some real requirement come. | |
| // `nameMap` can only be obtained from echarts option. | |
| // The original `named` must not be modified. | |
| // if (nameMap) { | |
| // svgGraphic = extend({}, svgGraphic); | |
| // svgGraphic.named = applyNameMap(svgGraphic.named, nameMap); | |
| // } | |
| return svgGraphic; | |
| } | |
| freeGraphic(hostKey: string): void { | |
| const usedRootMap = this._usedGraphicMap; | |
| const svgGraphic = usedRootMap.get(hostKey); | |
| if (svgGraphic) { | |
| usedRootMap.removeKey(hostKey); | |
| this._freedGraphics.push(svgGraphic); | |
| } | |
| } | |
| } | |
| function setSilent(el: Element): void { | |
| // Only named element has silent: false, other elements should | |
| // act as background and has no user interaction. | |
| el.silent = false; | |
| // text|tspan will be converted to group. | |
| if (el.isGroup) { | |
| el.traverse(child => { | |
| child.silent = false; | |
| }); | |
| } | |
| } | |
| function createRegions( | |
| named: SVGParserResultNamedItem[] | |
| ): { | |
| regions: GeoSVGRegion[]; | |
| regionsMap: HashMap<GeoSVGRegion>; | |
| } { | |
| const regions: GeoSVGRegion[] = []; | |
| const regionsMap = createHashMap<GeoSVGRegion>(); | |
| // Create resions only for the first graphic. | |
| each(named, namedItem => { | |
| // Region has feature to calculate center for tooltip or other features. | |
| // If there is a <g name="xxx">, the center should be the center of the | |
| // bounding rect of the g. | |
| if (namedItem.namedFrom != null) { | |
| return; | |
| } | |
| const region = new GeoSVGRegion(namedItem.name, namedItem.el); | |
| // PENDING: if `nameMap` supported, this region can not be mounted on | |
| // `this`, but can only be created each time `load()` called. | |
| regions.push(region); | |
| // PENDING: if multiple tag named with the same name, only one will be | |
| // found by `_regionsMap`. `_regionsMap` is used to find a coordinate | |
| // by name. We use `region.getCenter()` as the coordinate. | |
| regionsMap.set(namedItem.name, region); | |
| }); | |
| return { regions, regionsMap }; | |
| } | |
| // PENDING: `nameMap` will not be supported until some real requirement come. | |
| // /** | |
| // * Use the alias in geoNameMap. | |
| // * The input `named` must not be modified. | |
| // */ | |
| // function applyNameMap( | |
| // named: GeoSVGGraphicRecord['named'], | |
| // nameMap: NameMap | |
| // ): GeoSVGGraphicRecord['named'] { | |
| // const result = [] as GeoSVGGraphicRecord['named']; | |
| // for (let i = 0; i < named.length; i++) { | |
| // let regionGraphic = named[i]; | |
| // const name = regionGraphic.name; | |
| // if (nameMap && nameMap.hasOwnProperty(name)) { | |
| // regionGraphic = extend({}, regionGraphic); | |
| // regionGraphic.name = name; | |
| // } | |
| // result.push(regionGraphic); | |
| // } | |
| // return result; | |
| // } |