blob: e0e6d5900bacf4f3a6d2f39f2b608b5f0e178d18 [file] [log] [blame]
/*
* 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 becuase 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 suport `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;
// }