This is an automated email from the ASF dual-hosted git repository. sushuang pushed a commit to branch fix/geo-svg in repository https://gitbox.apache.org/repos/asf/echarts.git
commit ca5817445241ab7ccc55853ac8049b662b632449 Author: 100pah <[email protected]> AuthorDate: Tue Mar 9 12:51:20 2021 +0800 feature: [geo] support geo svg named elements have the same behavior as regions of geoJSON --- src/component/helper/MapDraw.ts | 386 +++++++++++++++++++++++----------------- src/coord/geo/GeoModel.ts | 1 - src/coord/geo/GeoSVGResource.ts | 6 +- test/geo-svg.html | 170 ++++++++++++++++++ 4 files changed, 398 insertions(+), 165 deletions(-) diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index 1ca9ad9..a7176bf 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -39,11 +39,24 @@ import { getECData } from '../../util/innerStore'; import { createOrUpdatePatternFromDecal } from '../../util/decal'; import { ViewCoordSysTransformInfoPart } from '../../coord/View'; import { GeoSVGResource } from '../../coord/geo/GeoSVGResource'; +import Displayable from 'zrender/src/graphic/Displayable'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; +import List from '../../data/List'; interface RegionsGroup extends graphic.Group { } +interface ViewBuildContext { + api: ExtensionAPI; + geo: Geo; + mapOrGeoModel: GeoModel | MapSeries; + data: List; + isVisualEncodedByVisualMap: boolean; + isGeo: boolean; + transformInfoRaw: ViewCoordSysTransformInfoPart; +} + function getFixedItemStyle(model: Model<GeoItemStyleOption>) { const itemStyle = model.getItemStyle(); const areaColor = model.get('areaColor'); @@ -78,12 +91,14 @@ class MapDraw { */ private _mouseDownFlag: boolean; - private _mapName: string; + private _svgMapName: string; private _regionsGroup: RegionsGroup; private _svgGroup: graphic.Group; + private _svgNamedElements: Displayable[]; + constructor(api: ExtensionAPI) { const group = new graphic.Group(); @@ -117,7 +132,6 @@ class MapDraw { const geo = mapOrGeoModel.coordinateSystem; - const regionsGroup = this._regionsGroup; const group = this.group; @@ -125,8 +139,6 @@ class MapDraw { const transformInfoRaw = transformInfo.raw; const transformInfoRoam = transformInfo.roam; - this._updateSVG(geo, transformInfoRaw); - // No animation when first draw or in action const isFirstDraw = !regionsGroup.childAt(0) || payload; @@ -141,17 +153,44 @@ class MapDraw { graphic.updateProps(group, transformInfoRoam, mapOrGeoModel); } - regionsGroup.removeAll(); - - const nameMap = zrUtil.createHashMap<RegionsGroup>(); - - const isVisualEncodedByVisualMap = data && data.getVisual('visualMeta') && data.getVisual('visualMeta').length > 0; + const viewBuildCtx = { + api, + geo, + mapOrGeoModel, + data, + isVisualEncodedByVisualMap, + isGeo, + transformInfoRaw + }; + + this._buildGeoJSON(viewBuildCtx); + this._buildSVG(viewBuildCtx); + + this._updateController(mapOrGeoModel, ecModel, api); + + this._updateMapSelectHandler(mapOrGeoModel, regionsGroup, api, fromView); + } + + private _buildGeoJSON(viewBuildCtx: ViewBuildContext): void { + const nameMap = zrUtil.createHashMap<RegionsGroup>(); + const regionsGroup = this._regionsGroup; + const transformInfoRaw = viewBuildCtx.transformInfoRaw; + + const transformPoint = function (point: number[]): number[] { + return [ + point[0] * transformInfoRaw.scaleX + transformInfoRaw.x, + point[1] * transformInfoRaw.scaleY + transformInfoRaw.y + ]; + }; + + regionsGroup.removeAll(); - zrUtil.each(geo.regions, function (region) { + // Only when the resource is GeoJSON, there is `geo.regions`. + zrUtil.each(viewBuildCtx.geo.regions, function (region) { // Consider in GeoJson properties.name may be duplicated, for example, // there is multiple region named "United Kindom" or "France" (so many @@ -169,50 +208,6 @@ class MapDraw { }); regionGroup.add(compoundPath); - const regionModel = mapOrGeoModel.getRegionModel(region.name) || mapOrGeoModel; - - // @ts-ignore FIXME:TS fix the "compatible with each other"? - const itemStyleModel = regionModel.getModel('itemStyle'); - // @ts-ignore FIXME:TS fix the "compatible with each other"? - const emphasisModel = regionModel.getModel('emphasis'); - const emphasisItemStyleModel = emphasisModel.getModel('itemStyle'); - // @ts-ignore FIXME:TS fix the "compatible with each other"? - const blurItemStyleModel = regionModel.getModel(['blur', 'itemStyle']); - // @ts-ignore FIXME:TS fix the "compatible with each other"? - const selectItemStyleModel = regionModel.getModel(['select', 'itemStyle']); - - // NOTE: DONT use 'style' in visual when drawing map. - // This component is used for drawing underlying map for both geo component and map series. - const itemStyle = getFixedItemStyle(itemStyleModel); - const emphasisItemStyle = getFixedItemStyle(emphasisItemStyleModel); - const blurItemStyle = getFixedItemStyle(blurItemStyleModel); - const selectItemStyle = getFixedItemStyle(selectItemStyleModel); - - let dataIdx; - // Use the itemStyle in data if has data - if (data) { - dataIdx = data.indexOfName(region.name); - // Only visual color of each item will be used. It can be encoded by visualMap - // But visual color of series is used in symbol drawing - // - // Visual color for each series is for the symbol draw - const style = data.getItemVisual(dataIdx, 'style'); - const decal = data.getItemVisual(dataIdx, 'decal'); - if (isVisualEncodedByVisualMap && style.fill) { - itemStyle.fill = style.fill; - } - if (decal) { - itemStyle.decal = createOrUpdatePatternFromDecal(decal, api); - } - } - - const transformPoint = function (point: number[]): number[] { - return [ - point[0] * transformInfoRaw.scaleX + transformInfoRaw.x, - point[1] * transformInfoRaw.scaleY + transformInfoRaw.y - ]; - }; - zrUtil.each(region.geometries, function (geometry) { if (geometry.type !== 'polygon') { return; @@ -243,149 +238,218 @@ class MapDraw { } }); - compoundPath.setStyle(itemStyle); - compoundPath.style.strokeNoScale = true; - compoundPath.culling = true; - - compoundPath.ensureState('emphasis').style = emphasisItemStyle; - compoundPath.ensureState('blur').style = blurItemStyle; - compoundPath.ensureState('select').style = selectItemStyle; - - let showLabel = false; - for (let i = 0; i < DISPLAY_STATES.length; i++) { - const stateName = DISPLAY_STATES[i]; - // @ts-ignore FIXME:TS fix the "compatible with each other"? - if (regionModel.get( - stateName === 'normal' ? ['label', 'show'] : [stateName, 'label', 'show'] - )) { - showLabel = true; - break; - } - } + const centerPt = transformPoint(region.center); - const isDataNaN = data && isNaN(data.get(data.mapDimension('value'), dataIdx) as number); - const itemLayout = data && data.getItemLayout(dataIdx); - - // In the following cases label will be drawn - // 1. In map series and data value is NaN - // 2. In geo component - // 4. Region has no series legendSymbol, which will be add a showLabel flag in mapSymbolLayout - if ( - (isGeo || isDataNaN && (showLabel)) - || (itemLayout && itemLayout.showLabel) - ) { - const query = !isGeo ? dataIdx : region.name; - let labelFetcher; - - // Consider dataIdx not found. - if (!data || dataIdx >= 0) { - labelFetcher = mapOrGeoModel; - } + this._resetSingleRegionGraphic( + viewBuildCtx, compoundPath, regionGroup, region.name, centerPt, null + ); - const centerPt = transformPoint(region.center); - const textEl = new graphic.Text({ - x: centerPt[0], - y: centerPt[1], - z2: 10, - silent: true - }); - textEl.afterUpdate = labelTextAfterUpdate; - - setLabelStyle<typeof query>( - textEl, getLabelStatesModels(regionModel), - { - labelFetcher: labelFetcher, - labelDataIndex: query, - defaultText: region.name - }, - { normal: { - align: 'center', - verticalAlign: 'middle' - } } - ); - - compoundPath.setTextContent(textEl); - compoundPath.setTextConfig({ - local: true - }); - - (compoundPath as ECElement).disableLabelAnimation = true; + regionsGroup.add(regionGroup); - } + }, this); + } + + private _buildSVG(viewBuildCtx: ViewBuildContext): void { + const mapName = viewBuildCtx.geo.map; + const transformInfoRaw = viewBuildCtx.transformInfoRaw; + + this._svgGroup.x = transformInfoRaw.x; + this._svgGroup.y = transformInfoRaw.y; + this._svgGroup.scaleX = transformInfoRaw.scaleX; + this._svgGroup.scaleY = transformInfoRaw.scaleY; + + if (this._svgResourceChanged(mapName)) { + this._freeSVG(); + this._useSVG(mapName); + } - // setItemGraphicEl, setHoverStyle after all polygons and labels - // are added to the rigionGroup - if (data) { - data.setItemGraphicEl(dataIdx, regionGroup); + zrUtil.each(this._svgNamedElements, function (namedElement) { + this._resetSingleRegionGraphic( + viewBuildCtx, namedElement, namedElement, namedElement.name, [0, 0], 'inside' + ); + }, this); + } + + private _resetSingleRegionGraphic( + viewBuildCtx: ViewBuildContext, + displayable: Displayable, + elForStateChange: Element, + regionName: string, + labelXY: number[], + labelPosition: ElementTextConfig['position'] + ): void { + + const mapOrGeoModel = viewBuildCtx.mapOrGeoModel; + const data = viewBuildCtx.data; + const isVisualEncodedByVisualMap = viewBuildCtx.isVisualEncodedByVisualMap; + const isGeo = viewBuildCtx.isGeo; + + const regionModel = mapOrGeoModel.getRegionModel(regionName) || mapOrGeoModel; + + // @ts-ignore FIXME:TS fix the "compatible with each other"? + const itemStyleModel = regionModel.getModel('itemStyle'); + // @ts-ignore FIXME:TS fix the "compatible with each other"? + const emphasisModel = regionModel.getModel('emphasis'); + const emphasisItemStyleModel = emphasisModel.getModel('itemStyle'); + // @ts-ignore FIXME:TS fix the "compatible with each other"? + const blurItemStyleModel = regionModel.getModel(['blur', 'itemStyle']); + // @ts-ignore FIXME:TS fix the "compatible with each other"? + const selectItemStyleModel = regionModel.getModel(['select', 'itemStyle']); + + // NOTE: DONT use 'style' in visual when drawing map. + // This component is used for drawing underlying map for both geo component and map series. + const itemStyle = getFixedItemStyle(itemStyleModel); + const emphasisItemStyle = getFixedItemStyle(emphasisItemStyleModel); + const blurItemStyle = getFixedItemStyle(blurItemStyleModel); + const selectItemStyle = getFixedItemStyle(selectItemStyleModel); + + let dataIdx; + // Use the itemStyle in data if has data + if (data) { + dataIdx = data.indexOfName(regionName); + // Only visual color of each item will be used. It can be encoded by visualMap + // But visual color of series is used in symbol drawing + // + // Visual color for each series is for the symbol draw + const style = data.getItemVisual(dataIdx, 'style'); + const decal = data.getItemVisual(dataIdx, 'decal'); + if (isVisualEncodedByVisualMap && style.fill) { + itemStyle.fill = style.fill; } - else { - const regionModel = mapOrGeoModel.getRegionModel(region.name); - // Package custom mouse event for geo component - getECData(compoundPath).eventData = { - componentType: 'geo', - componentIndex: mapOrGeoModel.componentIndex, - geoIndex: mapOrGeoModel.componentIndex, - name: region.name, - region: (regionModel && regionModel.option) || {} - }; + if (decal) { + itemStyle.decal = createOrUpdatePatternFromDecal(decal, viewBuildCtx.api); } + } + displayable.setStyle(itemStyle); + displayable.style.strokeNoScale = true; + displayable.culling = true; + + displayable.ensureState('emphasis').style = emphasisItemStyle; + displayable.ensureState('blur').style = blurItemStyle; + displayable.ensureState('select').style = selectItemStyle; + + + let showLabel = false; + for (let i = 0; i < DISPLAY_STATES.length; i++) { + const stateName = DISPLAY_STATES[i]; // @ts-ignore FIXME:TS fix the "compatible with each other"? - regionGroup.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode'); - enableHoverEmphasis(regionGroup, emphasisModel.get('focus'), emphasisModel.get('blurScope')); + if (regionModel.get( + stateName === 'normal' ? ['label', 'show'] : [stateName, 'label', 'show'] + )) { + showLabel = true; + break; + } + } - regionsGroup.add(regionGroup); - }); + const isDataNaN = data && isNaN(data.get(data.mapDimension('value'), dataIdx) as number); + const itemLayout = data && data.getItemLayout(dataIdx); + + // In the following cases label will be drawn + // 1. In map series and data value is NaN + // 2. In geo component + // 4. Region has no series legendSymbol, which will be add a showLabel flag in mapSymbolLayout + if ( + (isGeo || isDataNaN && (showLabel)) + || (itemLayout && itemLayout.showLabel) + ) { + const query = !isGeo ? dataIdx : regionName; + let labelFetcher; + + // Consider dataIdx not found. + if (!data || dataIdx >= 0) { + labelFetcher = mapOrGeoModel; + } - this._updateController(mapOrGeoModel, ecModel, api); + const textEl = new graphic.Text({ + x: labelXY[0], + y: labelXY[1], + z2: 10, + silent: true + }); + textEl.afterUpdate = labelTextAfterUpdate; + + setLabelStyle<typeof query>( + textEl, getLabelStatesModels(regionModel), + { + labelFetcher: labelFetcher, + labelDataIndex: query, + defaultText: regionName + }, + { normal: { + align: 'center', + verticalAlign: 'middle' + } } + ); + + displayable.setTextContent(textEl); + displayable.setTextConfig({ + local: true, + insideFill: textEl.style.fill, + position: labelPosition + }); + + (displayable as ECElement).disableLabelAnimation = true; + } + + + // setItemGraphicEl, setHoverStyle after all polygons and labels + // are added to the rigionGroup + if (data) { + data.setItemGraphicEl(dataIdx, elForStateChange); + } + else { + const regionModel = mapOrGeoModel.getRegionModel(regionName); + // Package custom mouse event for geo component + getECData(displayable).eventData = { + componentType: 'geo', + componentIndex: mapOrGeoModel.componentIndex, + geoIndex: mapOrGeoModel.componentIndex, + name: regionName, + region: (regionModel && regionModel.option) || {} + }; + } + + // @ts-ignore FIXME:TS fix the "compatible with each other"? + elForStateChange.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode'); + enableHoverEmphasis(elForStateChange, emphasisModel.get('focus'), emphasisModel.get('blurScope')); - this._updateMapSelectHandler(mapOrGeoModel, regionsGroup, api, fromView); } remove(): void { this._regionsGroup.removeAll(); this._svgGroup.removeAll(); this._controller.dispose(); - this._freeSVG(this._mapName); - this._mapName = null; + this._freeSVG(); this._controllerHost = null; } - private _updateSVG(geo: Geo, transformInfoRaw: ViewCoordSysTransformInfoPart): void { - const mapName = geo.map; - - this._svgGroup.x = transformInfoRaw.x; - this._svgGroup.y = transformInfoRaw.y; - this._svgGroup.scaleX = transformInfoRaw.scaleX; - this._svgGroup.scaleY = transformInfoRaw.scaleY; - - if (this._mapName !== mapName) { - this._freeSVG(this._mapName); - this._useSVG(mapName); - this._mapName = mapName; - } + private _svgResourceChanged(mapName: string): boolean { + return this._svgMapName !== mapName; } - private _useSVG(mapName: string) { - if (mapName == null) { - return; - } + private _useSVG(mapName: string): void { const resource = geoSourceManager.getGeoResource(mapName); if (resource && resource.type === 'svg') { const svgGraphic = (resource as GeoSVGResource).useGraphic(this.uid); this._svgGroup.add(svgGraphic.root); + this._svgNamedElements = svgGraphic.namedElements; + this._svgMapName = mapName; } } - private _freeSVG(mapName: string) { + private _freeSVG(): void { + const mapName = this._svgMapName; if (mapName == null) { return; } const resource = geoSourceManager.getGeoResource(mapName); if (resource && resource.type === 'svg') { (resource as GeoSVGResource).freeGraphic(this.uid); - this._svgGroup.removeAll(); } + this._svgGroup.removeAll(); + this._svgNamedElements = null; + this._svgMapName = null; } private _updateController( diff --git a/src/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts index 3705304..78054c6 100644 --- a/src/coord/geo/GeoModel.ts +++ b/src/coord/geo/GeoModel.ts @@ -163,7 +163,6 @@ class GeoModel extends ComponentModel<GeoOption> { }, itemStyle: { - // color: 各异, borderWidth: 0.5, borderColor: '#444', color: '#eee' diff --git a/src/coord/geo/GeoSVGResource.ts b/src/coord/geo/GeoSVGResource.ts index 0a3a4ec..43c18fa 100644 --- a/src/coord/geo/GeoSVGResource.ts +++ b/src/coord/geo/GeoSVGResource.ts @@ -24,11 +24,11 @@ import {assert, createHashMap, HashMap} from 'zrender/src/core/util'; import BoundingRect from 'zrender/src/core/BoundingRect'; import { GeoResource, GeoSVGSourceInput } from './geoTypes'; import { parseXML } from 'zrender/src/tool/parseXML'; -import Element from 'zrender/src/Element'; +import Displayable from 'zrender/src/graphic/Displayable'; export interface GeoSVGGraphic { root: Group; - namedElements: Element[]; + namedElements: Displayable[]; } export class GeoSVGResource implements GeoResource { @@ -115,7 +115,7 @@ function buildGraphic( ): { root: Group; boundingRect: BoundingRect; - namedElements: Element[] + namedElements: Displayable[] } { let result; let root; diff --git a/test/geo-svg.html b/test/geo-svg.html new file mode 100644 index 0000000..d7568d5 --- /dev/null +++ b/test/geo-svg.html @@ -0,0 +1,170 @@ +<!DOCTYPE html> +<!-- +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. +--> + + +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="lib/esl.js"></script> + <script src="lib/config.js"></script> + <script src="lib/jquery.min.js"></script> + <script src="lib/facePrint.js"></script> + <script src="lib/testHelper.js"></script> + <!-- <script src="ut/lib/canteen.js"></script> --> + <link rel="stylesheet" href="lib/reset.css" /> + </head> + <body> + <style> + </style> + + + + <div id="main0"></div> + <div id="main1"></div> + + + + + + + <script> + require(['echarts'/*, 'map/js/china' */], function (echarts) { + const testGeoJson1 = { + 'type': 'FeatureCollection', + 'features': [ + { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [ + [[2000, 2000], [5000, 2000], [5000, 5000], [2000, 5000]] + ] + }, + 'properties': { + 'name': 'Afghanistan', + 'childNum': 1 + } + } + ] + }; + + echarts.registerMap('testGeoJson1', testGeoJson1); + + option = { + geo: { + map: 'testGeoJson1', + roam: true, + // height: '100%', + // center + // layoutCenter: ['30%', 40], + // layoutSize: 40, + // boundingCoords + zoom: 1, + aspectScale: 1 + } + }; + + var chart = testHelper.create(echarts, 'main0', { + title: [ + 'geoJSON location:', + 'Should be a square and 80% of canvas height.', + 'At the center of the canvas.' + ], + option: option, + height: 300 + }); + + + }); + </script> + + + + <script> + require(['echarts'/*, 'map/js/china' */], function (echarts) { + var option; + $.ajax({ + url: '../../vis-data/map/svg/seats/seatmap-example.svg', // 剧场例子 + // url: '../../vis-data/map/svg/seats/Ethiopian_Airlines_Flight_961_seating_plan.svg', // 飞机例子 + // url: '../../vis-data/map/svg/seats/oracle-seating-map-2017-2.svg', // 渲染错误 + // url: '../../vis-data/map/svg/seats/DC-10-30_seat_configuration_chart.svg', // 渲染错误 + // url: '../../vis-data/map/svg/seats/Airbus_A300B4-622R_seat_configuration_chart.svg', // 渲染错误 + dataType: 'text' + }).done(function (svg) { + + echarts.registerMap('seatmap', { + svg: svg + }); + + option = { + geo: { + map: 'seatmap', + roam: true, + // height: 100, + // zoom: 1.5 + emphasis: { + // itemStyle: { + // color: 'red' + // }, + label: { + // color: '#fff', + textBorderColor: '#fff', + textBorderWidth: 2 + } + }, + // itemStyle: { + // color: 'red' + // }, + // label: { + // color: '#fff' + // } + }, + // series: { + // type: 'scatter', + // coordinateSystem: 'geo', + // // ????????????????????????? + // geoIndex: 0, + // data: [[11, 22], [33, 44]] + // } + }; + + var chart = testHelper.create(echarts, 'main1', { + title: [ + 'Test Case Description of main0', + '(Muliple lines and **emphasis** are supported in description)' + ], + option: option, + height: 300 + // buttons: [{text: 'btn-txt', onclick: function () {}}], + // recordCanvas: true, + }); + + }); + + }); + </script> + + + + + </body> +</html> + --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
