提交 fc4ca578 编写于 作者: 1 100pah

fix: [geo svg] fix viewBox and add test cases.

上级 d8ebb5fd
......@@ -108,7 +108,7 @@ export class GeoSVGResource implements GeoResource {
// in which there is no "view" info, so that it should better not to
// make references to graphic elements.
if (!firstGraphic) {
firstGraphic = this._firstGraphic = buildGraphic(this._parsedXML);
firstGraphic = this._firstGraphic = this._buildGraphic(this._parsedXML);
this._freedGraphics.push(firstGraphic);
......@@ -131,6 +131,126 @@ export class GeoSVGResource implements GeoResource {
};
}
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;
}
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.
......@@ -151,7 +271,7 @@ export class GeoSVGResource implements GeoResource {
svgGraphic = this._freedGraphics.pop()
// use the first boundingRect to avoid duplicated boundingRect calculation.
|| buildGraphic(this._parsedXML, this._boundingRect);
|| this._buildGraphic(this._parsedXML);
usedRootMap.set(hostKey, svgGraphic);
......@@ -179,75 +299,6 @@ export class GeoSVGResource implements GeoResource {
}
function buildGraphic(
svgXML: SVGElement,
// If input boundingRect, avoid boundingRect calculation,
// which might be time-consuming.
boundingRect?: BoundingRect
): GeoSVGGraphicRecord {
let result;
let root;
try {
result = svgXML && parseSVG(svgXML, {
ignoreViewBox: true,
ignoreRootClip: true
}) || {};
root = result.root;
assert(root != null);
}
catch (e) {
throw new Error('Invalid svg format\n' + e.message);
}
const svgWidth = result.width;
const svgHeight = result.height;
const viewBoxRect = result.viewBoxRect;
if (!boundingRect) {
boundingRect = (svgWidth == null || svgHeight == null)
// If svg width / height not specified, calculate
// bounding rect as the width / height
? root.getBoundingRect()
: new BoundingRect(0, 0, 0, 0);
if (svgWidth != null) {
boundingRect.width = svgWidth;
}
if (svgHeight != null) {
boundingRect.height = svgHeight;
}
}
// Note: we keep the covenant that the root has no transform.
if (viewBoxRect) {
const viewBoxTransform = makeViewBoxTransform(viewBoxRect, boundingRect.width, boundingRect.height);
const elRoot = root;
root = new Group();
root.add(elRoot);
elRoot.scaleX = elRoot.scaleY = viewBoxTransform.scale;
elRoot.x = viewBoxTransform.x;
elRoot.y = viewBoxTransform.y;
}
root.setClipPath(new Rect({
shape: boundingRect.plain()
}));
(root as GeoSVGGraphicRoot).isGeoSVGGraphicRoot = true;
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 };
}
function setSilent(el: Element): void {
// Only named element has silent: false, other elements should
// act as background and has no user interaction.
......
此差异已折叠。
......@@ -40,6 +40,7 @@ under the License.
<div id="main_simple_geo_json"></div>
<div id="main_simple_geo_svg"></div>
<div id="main_simple_geo_svg_coord"></div>
<div id="main_simple_geo_svg_viewBox_svgWH"></div>
<div id="main_geo_json_focus_blur"></div>
<div id="main_geo_svg_emphasis_select"></div>
<div id="main_pure_geo_svg"></div>
......@@ -298,6 +299,138 @@ under the License.
<script>
require(['echarts'], function (echarts) {
const xmlHeader = '<?xml version="1.0" encoding="utf-8"?>';
const svgTagCommonAttr = ' xmlns="http://www.w3.org/2000/svg" xmlns:ooo="http://xml.openoffice.org/svg/export" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" fill-rule="evenodd" xml:space="preserve" ';
// Both svgWdith svgHeight viewBox
const svg1 = [
xmlHeader,
'<svg ' + svgTagCommonAttr,
'viewBox="99500 99500 1000 1000"',
'width="5px" height="5px"',
'>',
'<circle cx="100000" cy="100000" r="500" fill="blue" stroke-linecap="square" stroke-linejoin="miter"/>',
'</svg>'
].join(' ');
// Both svgWdith svgHeight viewBox and clip
const svg2 = [
xmlHeader,
'<svg ' + svgTagCommonAttr,
'viewBox="99500 99500 500 500"',
'width="5px" height="5px"',
'>',
'<circle cx="100000" cy="100000" r="500" fill="blue" stroke-linecap="square" stroke-linejoin="miter"/>',
'</svg>'
].join(' ');
// Only svgWdith svgHeight
const svg3 = [
xmlHeader,
'<svg ' + svgTagCommonAttr,
'width="100000" height="100000"',
'>',
'<circle cx="100000" cy="100000" r="100000" fill="blue" stroke-linecap="square" stroke-linejoin="miter"/>',
'</svg>'
].join(' ');
// Only viewBox
const svg4 = [
xmlHeader,
'<svg ' + svgTagCommonAttr,
'viewBox="99500 99500 500 500"',
'>',
'<circle cx="100000" cy="100000" r="500" fill="blue" stroke-linecap="square" stroke-linejoin="miter"/>',
'</svg>'
].join(' ');
// Neither viewBox and svgWidth svgHeight
const svg5 = [
xmlHeader,
'<svg ' + svgTagCommonAttr,
'>',
'<circle cx="100000" cy="100000" r="500" fill="blue" stroke-linecap="square" stroke-linejoin="miter"/>',
'</svg>'
].join(' ');
// Both viewBox but only svgHeight no svgWidth
const svg6 = [
xmlHeader,
'<svg ' + svgTagCommonAttr,
'viewBox="99500 99500 1000 1000"',
'height="500"',
'>',
'<circle cx="100000" cy="100000" r="500" fill="blue" stroke-linecap="square" stroke-linejoin="miter"/>',
'</svg>'
].join(' ');
echarts.registerMap('testGeoSVG_viewBox_svgWH_1', { svg: svg1 });
echarts.registerMap('testGeoSVG_viewBox_svgWH_2', { svg: svg2 });
echarts.registerMap('testGeoSVG_viewBox_svgWH_3', { svg: svg3 });
echarts.registerMap('testGeoSVG_viewBox_svgWH_4', { svg: svg4 });
echarts.registerMap('testGeoSVG_viewBox_svgWH_5', { svg: svg5 });
echarts.registerMap('testGeoSVG_viewBox_svgWH_6', { svg: svg6 });
const RECT_SIZE = 50;
const Y_TOP = 10;
const X_LEFT = 10;
const GAP = 20;
var _itemIndex = 0;
var _graphicOptionArr = [];
var _geoOptionArr = [];
function makeItem(text, mapType) {
var x = X_LEFT + (RECT_SIZE + GAP) * _itemIndex++;
_graphicOptionArr.push({
type: 'rect',
silent: true,
shape: { x: x, y: Y_TOP, width: RECT_SIZE, height: RECT_SIZE },
style: { stroke: 'red', lineWidth: 1, fill: null }
}, {
type: 'text',
silent: true,
style: {
text: text, x: x + RECT_SIZE / 2, y: Y_TOP + RECT_SIZE + GAP,
fill: '#000', textAlign: 'center', textVerticalAlign: 'middle'
}
});
_geoOptionArr.push({
map: mapType,
left: x, top: Y_TOP, width: RECT_SIZE, height: RECT_SIZE,
itemStyle: { color: 'blue' }
});
}
makeItem('circle', 'testGeoSVG_viewBox_svgWH_1');
makeItem('1/4 sector', 'testGeoSVG_viewBox_svgWH_2');
makeItem('1/4 sector', 'testGeoSVG_viewBox_svgWH_3');
makeItem('1/4 sector', 'testGeoSVG_viewBox_svgWH_4');
makeItem('circle', 'testGeoSVG_viewBox_svgWH_5');
makeItem('thin ellipse', 'testGeoSVG_viewBox_svgWH_6');
option = {
animation: false,
tooltip: {
},
graphic: _graphicOptionArr,
geo: _geoOptionArr
};
var chart = testHelper.create(echarts, 'main_simple_geo_svg_viewBox_svgWH', {
title: [
'viewBox test:',
'Check each shape should be inside each red rect.',
'Check each shape be correspondence with the text.',
],
option: option,
height: 100
});
});
</script>
<script>
require(['echarts'/*, 'map/js/china' */], function (echarts) {
const testGeoJson1 = {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册