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 e9af050807d890ed99ddf6ecf8fa891993ca5e22 Author: 100pah <[email protected]> AuthorDate: Sun Mar 7 01:06:13 2021 +0800 fix: [geo] (1) refactor of geoJSON & SVG source loader and management. (2) remove the support of "load multiple geoJSON or SVG in one map name". This feature is never documented and published. And this feature have a inherent problem that it's not easy to normalize the unit among the different SVG. After this commit, one map name can only have one geoJSON or one SVG. --- src/component/helper/MapDraw.ts | 50 +++-- src/coord/View.ts | 6 +- src/coord/geo/GeoJSONResource.ts | 166 ++++++++++++++++ .../geo/{geoSVGLoader.ts => GeoSVGResource.ts} | 115 ++++++----- src/coord/geo/geoCreator.ts | 6 +- src/coord/geo/geoJSONLoader.ts | 99 ---------- src/coord/geo/geoSourceManager.ts | 217 +++++++++++---------- src/coord/geo/geoTypes.ts | 19 ++ src/coord/geo/mapDataStorage.ts | 158 --------------- src/core/echarts.ts | 21 +- 10 files changed, 416 insertions(+), 441 deletions(-) diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index e1a8f47..79bc1cc 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -38,10 +38,12 @@ import Model from '../../model/Model'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import { getECData } from '../../util/innerStore'; import { createOrUpdatePatternFromDecal } from '../../util/decal'; +import { ViewCoordSysTransformInfoPart } from '../../coord/View'; +import { GeoSVGResource } from '../../coord/geo/GeoSVGResource'; interface RegionsGroup extends graphic.Group { - __regions: Region[]; + __regions: Region[] } function getFixedItemStyle(model: Model<GeoItemStyleOption>) { @@ -82,7 +84,7 @@ class MapDraw { private _regionsGroup: RegionsGroup; - private _backgroundGroup: graphic.Group; + private _svgGroup: graphic.Group; constructor(api: ExtensionAPI) { @@ -93,7 +95,7 @@ class MapDraw { this.group = group; group.add(this._regionsGroup = new graphic.Group() as RegionsGroup); - group.add(this._backgroundGroup = new graphic.Group()); + group.add(this._svgGroup = new graphic.Group()); } draw( @@ -117,7 +119,6 @@ class MapDraw { const geo = mapOrGeoModel.coordinateSystem; - this._updateBackground(geo); const regionsGroup = this._regionsGroup; const group = this.group; @@ -126,6 +127,8 @@ 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; @@ -356,23 +359,48 @@ class MapDraw { remove(): void { this._regionsGroup.removeAll(); - this._backgroundGroup.removeAll(); + this._svgGroup.removeAll(); this._controller.dispose(); - this._mapName && geoSourceManager.removeGraphic(this._mapName, this.uid); + this._freeSVG(this._mapName); this._mapName = null; this._controllerHost = null; } - private _updateBackground(geo: Geo): void { + 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) { - zrUtil.each(geoSourceManager.makeGraphic(mapName, this.uid), function (root) { - this._backgroundGroup.add(root); - }, this); + this._freeSVG(this._mapName); + this._useSVG(mapName); + this._mapName = mapName; } + } - this._mapName = mapName; + private _useSVG(mapName: string) { + if (mapName == null) { + return; + } + const resource = geoSourceManager.getGeoResource(mapName); + if (resource && resource.type === 'svg') { + const root = (resource as GeoSVGResource).useGraphic(this.uid); + this._svgGroup.add(root); + } + } + + private _freeSVG(mapName: string) { + if (mapName == null) { + return; + } + const resource = geoSourceManager.getGeoResource(mapName); + if (resource && resource.type === 'svg') { + (resource as GeoSVGResource).freeGraphic(this.uid); + this._svgGroup.removeAll(); + } } private _updateController( diff --git a/src/coord/View.ts b/src/coord/View.ts index 6a72a3f..93fa411 100644 --- a/src/coord/View.ts +++ b/src/coord/View.ts @@ -32,6 +32,8 @@ import { ParsedModelFinder, ParsedModelFinderKnown } from '../util/model'; const v2ApplyTransform = vector.applyTransform; +export type ViewCoordSysTransformInfoPart = Pick<Transformable, 'x' | 'y' | 'scaleX' | 'scaleY'>; + class View extends Transformable implements CoordinateSystemMaster, CoordinateSystem { readonly type: string = 'view'; @@ -219,8 +221,8 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy } getTransformInfo(): { - roam: Pick<Transformable, 'x' | 'y' | 'scaleX' | 'scaleY'> - raw: Pick<Transformable, 'x' | 'y' | 'scaleX' | 'scaleY'> + roam: ViewCoordSysTransformInfoPart + raw: ViewCoordSysTransformInfoPart } { const roamTransformable = this._roamTransformable; const rawTransformable = this._rawTransformable; diff --git a/src/coord/geo/GeoJSONResource.ts b/src/coord/geo/GeoJSONResource.ts new file mode 100644 index 0000000..6189322 --- /dev/null +++ b/src/coord/geo/GeoJSONResource.ts @@ -0,0 +1,166 @@ +/* +* 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 { each, isString, createHashMap } from 'zrender/src/core/util'; +import parseGeoJson from './parseGeoJson'; +// Built-in GEO fixer. +import fixNanhai from './fix/nanhai'; +import fixTextCoord from './fix/textCoord'; +import fixGeoCoord from './fix/geoCoord'; +import fixDiaoyuIsland from './fix/diaoyuIsland'; +import BoundingRect from 'zrender/src/core/BoundingRect'; +import Region from './Region'; +import { GeoJSON, GeoJSONCompressed, GeoJSONSourceInput, GeoResource, GeoSpecialAreas, NameMap } from './geoTypes'; + + +export class GeoJSONResource implements GeoResource { + + readonly type = 'geoJSON'; + private _geoJSON: GeoJSON | GeoJSONCompressed; + private _specialAreas: GeoSpecialAreas; + private _mapName: string; + + private _parsed: { + regions: Region[]; + boundingRect: BoundingRect; + }; + + constructor( + mapName: string, + geoJSON: GeoJSONSourceInput, + specialAreas: GeoSpecialAreas + ) { + this._mapName = mapName; + this._specialAreas = specialAreas; + + // PENDING: delay the parse to the first usage to rapid up the FMP? + this._geoJSON = parseInput(geoJSON); + } + + load(nameMap: NameMap, nameProperty: string) { + + let parsed = this._parsed; + if (!parsed) { + const rawRegions = this._parseToRegions(nameProperty); + parsed = this._parsed = { + regions: rawRegions, + boundingRect: calculateBoundingRect(rawRegions) + }; + } + + const regionsMap = createHashMap<Region>(); + const nameCoordMap = createHashMap<Region['center']>(); + + const finalRegions: Region[] = []; + each(parsed.regions, function (region) { + let regionName = region.name; + + // Try use the alias in geoNameMap + if (nameMap && nameMap.hasOwnProperty(regionName)) { + region = region.cloneShallow(regionName = nameMap[regionName]); + } + + finalRegions.push(region); + regionsMap.set(regionName, region); + nameCoordMap.set(regionName, region.center); + }); + + return { + regions: finalRegions, + boundingRect: parsed.boundingRect || new BoundingRect(0, 0, 0, 0), + regionsMap: regionsMap, + nameCoordMap: nameCoordMap + }; + } + + private _parseToRegions(nameProperty: string): Region[] { + const mapName = this._mapName; + const geoJSON = this._geoJSON; + let rawRegions; + + // https://jsperf.com/try-catch-performance-overhead + try { + rawRegions = geoJSON ? parseGeoJson(geoJSON, nameProperty) : []; + } + catch (e) { + throw new Error('Invalid geoJson format\n' + e.message); + } + + fixNanhai(mapName, rawRegions); + + each(rawRegions, function (region) { + const regionName = region.name; + + fixTextCoord(mapName, region); + fixGeoCoord(mapName, region); + fixDiaoyuIsland(mapName, region); + + // Some area like Alaska in USA map needs to be tansformed + // to look better + const specialArea = this._specialAreas && this._specialAreas[regionName]; + if (specialArea) { + region.transformTo( + specialArea.left, specialArea.top, specialArea.width, specialArea.height + ); + } + }, this); + + return rawRegions; + } + + /** + * Only for exporting to users. + * **MUST NOT** used internally. + */ + getMapForUser(): { + // backward compat. + geoJson: GeoJSON | GeoJSONCompressed; + geoJSON: GeoJSON | GeoJSONCompressed; + specialAreas: GeoSpecialAreas; + } { + return { + // For backward compatibility, use geoJson + // PENDING: it has been returning them without clone. + // do we need to avoid outsite modification? + geoJson: this._geoJSON, + geoJSON: this._geoJSON, + specialAreas: this._specialAreas + }; + } + +} + +function calculateBoundingRect(regions: Region[]): BoundingRect { + let rect; + for (let i = 0; i < regions.length; i++) { + const regionRect = regions[i].getBoundingRect(); + rect = rect || regionRect.clone(); + rect.union(regionRect); + } + return rect; +} + +function parseInput(source: GeoJSONSourceInput): GeoJSON | GeoJSONCompressed { + return !isString(source) + ? source + : (typeof JSON !== 'undefined' && JSON.parse) + ? JSON.parse(source) + : (new Function('return (' + source + ');'))(); +} diff --git a/src/coord/geo/geoSVGLoader.ts b/src/coord/geo/GeoSVGResource.ts similarity index 50% rename from src/coord/geo/geoSVGLoader.ts rename to src/coord/geo/GeoSVGResource.ts index 349dbb7..14a29c3 100644 --- a/src/coord/geo/geoSVGLoader.ts +++ b/src/coord/geo/GeoSVGResource.ts @@ -22,82 +22,95 @@ import Group from 'zrender/src/graphic/Group'; import Rect from 'zrender/src/graphic/shape/Rect'; import {assert, createHashMap, HashMap} from 'zrender/src/core/util'; import BoundingRect from 'zrender/src/core/BoundingRect'; -import {makeInner} from '../../util/model'; -import { SVGMapRecord } from './mapDataStorage'; +import { GeoResource, GeoSVGSourceInput } from './geoTypes'; +import { parseXML } from 'zrender/src/tool/parseXML'; -type MapRecordInner = { - originRoot: Group; - boundingRect: BoundingRect; - // key: hostKey, value: root - rootMap: HashMap<Group>; - originRootHostKey: string; -}; -const inner = makeInner<MapRecordInner, SVGMapRecord>(); +export class GeoSVGResource implements GeoResource { -export default { + readonly type = 'svg'; + private _mapName: string; + private _parsedXML: SVGElement; + private _rootForRect: Group; + private _boundingRect: BoundingRect; + // key: hostKey, value: root + private _usedRootMap: HashMap<Group> = createHashMap(); + private _freedRoots: Group[] = []; + + 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(mapName: string, mapRecord: SVGMapRecord): ReturnType<typeof buildGraphic> { - const originRoot = inner(mapRecord).originRoot; - if (originRoot) { - return { - root: originRoot, - boundingRect: inner(mapRecord).boundingRect - }; + load(): { boundingRect: BoundingRect } { + // In the "load" stage, graphic need to be built to + // get boundingRect for geo coordinate system. + const rootForRect = this._rootForRect; + if (rootForRect) { + return { boundingRect: this._boundingRect }; } - const graphic = buildGraphic(mapRecord); + const graphic = buildGraphic(this._parsedXML); - inner(mapRecord).originRoot = graphic.root; - inner(mapRecord).boundingRect = graphic.boundingRect; + this._rootForRect = graphic.root; + this._boundingRect = graphic.boundingRect; - return graphic; - }, + this._freedRoots.push(graphic.root); - makeGraphic(mapName: string, mapRecord: SVGMapRecord, hostKey: string): Group { - // For performance consideration (in large SVG), graphic only maked - // when necessary and reuse them according to hostKey. - const field = inner(mapRecord); - const rootMap = field.rootMap || (field.rootMap = createHashMap()); + return { boundingRect: graphic.boundingRect }; + } - let root = rootMap.get(hostKey); + // 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): Group { + const usedRootMap = this._usedRootMap; + + let root = usedRootMap.get(hostKey); if (root) { return root; } - const originRoot = field.originRoot; - const boundingRect = field.boundingRect; + root = this._freedRoots.pop() || buildGraphic(this._parsedXML, this._boundingRect).root; - // For performance, if originRoot is not used by a view, - // assign it to a view, but not reproduce graphic elements. - if (!field.originRootHostKey) { - field.originRootHostKey = hostKey; - root = originRoot; - } - else { - root = buildGraphic(mapRecord, boundingRect).root; - } + return usedRootMap.set(hostKey, root); + } - return rootMap.set(hostKey, root); - }, + freeGraphic(hostKey: string): void { + const usedRootMap = this._usedRootMap; - removeGraphic(mapName: string, mapRecord: SVGMapRecord, hostKey: string): void { - const field = inner(mapRecord); - const rootMap = field.rootMap; - rootMap && rootMap.removeKey(hostKey); - if (hostKey === field.originRootHostKey) { - field.originRootHostKey = null; + const root = usedRootMap.get(hostKey); + if (root) { + usedRootMap.removeKey(hostKey); + this._freedRoots.push(root); } } -}; + +} + function buildGraphic( - mapRecord: SVGMapRecord, boundingRect?: BoundingRect + svgXML: SVGElement, + boundingRect?: BoundingRect ): { root: Group; boundingRect: BoundingRect; } { - const svgXML = mapRecord.svgXML; let result; let root; diff --git a/src/coord/geo/geoCreator.ts b/src/coord/geo/geoCreator.ts index fad9400..e01f0d8 100644 --- a/src/coord/geo/geoCreator.ts +++ b/src/coord/geo/geoCreator.ts @@ -22,7 +22,6 @@ import Geo from './Geo'; import * as layout from '../../util/layout'; import * as numberUtil from '../../util/number'; import geoSourceManager from './geoSourceManager'; -import mapDataStorage from './mapDataStorage'; import GeoModel, { GeoOption, RegoinOption } from './GeoModel'; import MapSeries, { MapSeriesOption } from '../../chart/map/MapSeries'; import ExtensionAPI from '../../core/ExtensionAPI'; @@ -141,8 +140,9 @@ class GeoCreator implements CoordinateSystemCreator { let aspectScale = geoModel.get('aspectScale'); let invertLongitute = true; - const mapRecords = mapDataStorage.retrieveMap(name); - if (mapRecords && mapRecords[0] && mapRecords[0].type === 'svg') { + + const geoResource = geoSourceManager.getGeoResource(name); + if (geoResource.type === 'svg') { aspectScale == null && (aspectScale = 1); invertLongitute = false; } diff --git a/src/coord/geo/geoJSONLoader.ts b/src/coord/geo/geoJSONLoader.ts deleted file mode 100644 index 93e5c9e..0000000 --- a/src/coord/geo/geoJSONLoader.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* -* 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 {each} from 'zrender/src/core/util'; -import parseGeoJson from './parseGeoJson'; -import {makeInner} from '../../util/model'; - -// Built-in GEO fixer. -import fixNanhai from './fix/nanhai'; -import fixTextCoord from './fix/textCoord'; -import fixGeoCoord from './fix/geoCoord'; -import fixDiaoyuIsland from './fix/diaoyuIsland'; -import { GeoJSONMapRecord } from './mapDataStorage'; -import BoundingRect from 'zrender/src/core/BoundingRect'; -import Region from './Region'; - -type MapRecordInner = { - parsed: { - regions: Region[]; - boundingRect: BoundingRect; - }; -}; - -const inner = makeInner<MapRecordInner, GeoJSONMapRecord>(); - -export default { - - load(mapName: string, mapRecord: GeoJSONMapRecord, nameProperty: string): MapRecordInner['parsed'] { - - const parsed = inner(mapRecord).parsed; - - if (parsed) { - return parsed; - } - - const specialAreas = mapRecord.specialAreas || {}; - const geoJSON = mapRecord.geoJSON; - let regions; - - // https://jsperf.com/try-catch-performance-overhead - try { - regions = geoJSON ? parseGeoJson(geoJSON, nameProperty) : []; - } - catch (e) { - throw new Error('Invalid geoJson format\n' + e.message); - } - - fixNanhai(mapName, regions); - - each(regions, function (region) { - const regionName = region.name; - - fixTextCoord(mapName, region); - fixGeoCoord(mapName, region); - fixDiaoyuIsland(mapName, region); - - // Some area like Alaska in USA map needs to be tansformed - // to look better - const specialArea = specialAreas[regionName]; - if (specialArea) { - region.transformTo( - specialArea.left, specialArea.top, specialArea.width, specialArea.height - ); - } - }); - - return (inner(mapRecord).parsed = { - regions: regions, - boundingRect: getBoundingRect(regions) - }); - } -}; - -function getBoundingRect(regions: Region[]): BoundingRect { - let rect; - for (let i = 0; i < regions.length; i++) { - const regionRect = regions[i].getBoundingRect(); - rect = rect || regionRect.clone(); - rect.union(regionRect); - } - return rect; -} - diff --git a/src/coord/geo/geoSourceManager.ts b/src/coord/geo/geoSourceManager.ts index e324bbf..66cdf1e 100644 --- a/src/coord/geo/geoSourceManager.ts +++ b/src/coord/geo/geoSourceManager.ts @@ -17,122 +17,133 @@ * under the License. */ -import {each, createHashMap, HashMap} from 'zrender/src/core/util'; -import mapDataStorage, { MapRecord } from './mapDataStorage'; -import geoJSONLoader from './geoJSONLoader'; -import geoSVGLoader from './geoSVGLoader'; -import BoundingRect from 'zrender/src/core/BoundingRect'; -import { NameMap } from './geoTypes'; -import Region from './Region'; -import { Dictionary } from 'zrender/src/core/types'; -import Group from 'zrender/src/graphic/Group'; - - -interface Loader { - load: (mapName: string, mapRecord: MapRecord, nameProperty?: string) => { - regions?: Region[]; - boundingRect?: BoundingRect; - }; - makeGraphic?: (mapName: string, mapRecord: MapRecord, hostKey: string) => Group; - removeGraphic?: (mapName: string, mapRecord: MapRecord, hostKey: string) => void; +import { createHashMap } from 'zrender/src/core/util'; +import { GeoSVGResource } from './GeoSVGResource'; +import { + GeoJSON, + GeoJSONSourceInput, + GeoResource, + GeoSpecialAreas, + NameMap, + GeoSVGSourceInput +} from './geoTypes'; +import { GeoJSONResource } from './GeoJSONResource'; + + +type MapInput = GeoJSONMapInput | SVGMapInput; +interface GeoJSONMapInput { + geoJSON: GeoJSONSourceInput; + specialAreas: GeoSpecialAreas; } -const loaders = { - geoJSON: geoJSONLoader, - svg: geoSVGLoader -} as Dictionary<Loader>; +interface GeoJSONMapInputCompat extends GeoJSONMapInput { + geoJson: GeoJSONSourceInput; +} +interface SVGMapInput { + svg: GeoSVGSourceInput; +} + + +const storage = createHashMap<GeoResource>(); + export default { - load: function (mapName: string, nameMap: NameMap, nameProperty?: string): { - regions: Region[]; - // Key: mapName - regionsMap: HashMap<Region>; - // Key: mapName - nameCoordMap: HashMap<number[]>; - boundingRect: BoundingRect - } { - const regions = [] as Region[]; - const regionsMap = createHashMap<Region>(); - const nameCoordMap = createHashMap<Region['center']>(); - let boundingRect: BoundingRect; - const mapRecords = retrieveMap(mapName); - - each(mapRecords, function (record) { - const singleSource = loaders[record.type].load(mapName, record, nameProperty); - - each(singleSource.regions, function (region) { - let regionName = region.name; - - // Try use the alias in geoNameMap - if (nameMap && nameMap.hasOwnProperty(regionName)) { - region = region.cloneShallow(regionName = nameMap[regionName]); - } - - regions.push(region); - regionsMap.set(regionName, region); - nameCoordMap.set(regionName, region.center); - }); - - const rect = singleSource.boundingRect; - if (rect) { - boundingRect - ? boundingRect.union(rect) - : (boundingRect = rect.clone()); + /** + * Compatible with previous `echarts.registerMap`. + * + * @usage + * ```js + * + * echarts.registerMap('USA', geoJson, specialAreas); + * + * echarts.registerMap('USA', { + * geoJson: geoJson, + * specialAreas: {...} + * }); + * echarts.registerMap('USA', { + * geoJSON: geoJson, + * specialAreas: {...} + * }); + * + * echarts.registerMap('airport', { + * svg: svg + * } + * ``` + * + * Note: + * Do not support that register multiple geoJSON or SVG + * one map name. Because different geoJSON and SVG have + * different unit. It's not easy to make sure how those + * units are mapping/normalize. + * If intending to use multiple geoJSON or SVG, we can + * use multiple geo coordinate system. + */ + registerMap: function ( + mapName: string, + rawDef: MapInput | GeoJSONSourceInput, + rawSpecialAreas?: GeoSpecialAreas + ): void { + + if ((rawDef as SVGMapInput).svg) { + const resource = new GeoSVGResource( + mapName, + (rawDef as SVGMapInput).svg + ); + + storage.set(mapName, resource); + } + else { + // Recommend: + // echarts.registerMap('eu', { geoJSON: xxx, specialAreas: xxx }); + // Backward compatibility: + // echarts.registerMap('eu', geoJSON, specialAreas); + // echarts.registerMap('eu', { geoJson: xxx, specialAreas: xxx }); + let geoJSON = (rawDef as GeoJSONMapInputCompat).geoJson + || (rawDef as GeoJSONMapInput).geoJSON; + if (geoJSON && !(rawDef as GeoJSON).features) { + rawSpecialAreas = (rawDef as GeoJSONMapInput).specialAreas; + } + else { + geoJSON = rawDef as GeoJSONSourceInput; } - }); - - return { - regions: regions, - regionsMap: regionsMap, - nameCoordMap: nameCoordMap, - // FIXME Always return new ? - boundingRect: boundingRect || new BoundingRect(0, 0, 0, 0) - }; + const resource = new GeoJSONResource( + mapName, + geoJSON, + rawSpecialAreas + ); + + storage.set(mapName, resource); + } }, - /** - * @param hostKey For cache. - * @return Roots. - */ - makeGraphic: function (mapName: string, hostKey: string): Group[] { - const mapRecords = retrieveMap(mapName); - const results = [] as Group[]; - each(mapRecords, function (record) { - const method = loaders[record.type].makeGraphic; - method && results.push(method(mapName, record, hostKey)); - }); - return results; + getGeoResource(mapName: string): GeoResource { + return storage.get(mapName); }, /** - * @param hostKey For cache. + * Only for exporting to users. + * **MUST NOT** used internally. */ - removeGraphic: function (mapName: string, hostKey: string): void { - const mapRecords = retrieveMap(mapName); - each(mapRecords, function (record) { - const method = loaders[record.type].makeGraphic; - method && method(mapName, record, hostKey); - }); - } -}; - -function mapNotExistsError(mapName: string): void { - if (__DEV__) { - console.error( - 'Map ' + mapName + ' not exists. The GeoJSON of the map must be provided.' - ); - } -} + getMapForUser: function (mapName: string): ReturnType<GeoJSONResource['getMapForUser']> { + const resource = storage.get(mapName); + // Do not support return SVG until some real requirement come. + return resource && resource.type === 'geoJSON' + && (resource as GeoJSONResource).getMapForUser(); + }, -function retrieveMap(mapName: string): MapRecord[] { - const mapRecords = mapDataStorage.retrieveMap(mapName) || []; + load: function (mapName: string, nameMap: NameMap, nameProperty?: string) { + const resource = storage.get(mapName); - if (__DEV__) { - if (!mapRecords.length) { - mapNotExistsError(mapName); + if (!resource) { + if (__DEV__) { + console.error( + 'Map ' + mapName + ' not exists. The GeoJSON of the map must be provided.' + ); + } + return; } - } - return mapRecords; -} + return resource.load(nameMap, nameProperty); + } +}; diff --git a/src/coord/geo/geoTypes.ts b/src/coord/geo/geoTypes.ts index 910e0fe..52b8eb2 100644 --- a/src/coord/geo/geoTypes.ts +++ b/src/coord/geo/geoTypes.ts @@ -17,6 +17,13 @@ * under the License. */ +import BoundingRect from 'zrender/src/core/BoundingRect'; +import { HashMap } from 'zrender/src/core/util'; +import Region from './Region'; + + +export type GeoSVGSourceInput = 'string' | Document | SVGElement; +export type GeoJSONSourceInput = 'string' | GeoJSON | GeoJSONCompressed; export interface NameMap { [regionName: string]: string @@ -114,3 +121,15 @@ interface GeoJSONGeometryMultiPolygonCompressed { // type: 'GeometryCollection'; // geometries: GeoJSONGeometry[]; // }; + +export interface GeoResource { + readonly type: 'geoJSON' | 'svg'; + load(nameMap: NameMap, nameProperty: string): { + boundingRect: BoundingRect; + regions?: Region[]; + // Key: mapName + regionsMap?: HashMap<Region>; + // Key: mapName + nameCoordMap?: HashMap<number[]>; + }; +} diff --git a/src/coord/geo/mapDataStorage.ts b/src/coord/geo/mapDataStorage.ts deleted file mode 100644 index b9562f9..0000000 --- a/src/coord/geo/mapDataStorage.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* -* 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 {createHashMap, isString, isArray, each, assert} from 'zrender/src/core/util'; -import {parseXML} from 'zrender/src/tool/parseXML'; -import { GeoSpecialAreas, GeoJSON, GeoJSONCompressed } from './geoTypes'; -import { Dictionary } from 'zrender/src/core/types'; - -// For minimize the code size of common echarts package, -// do not put too much logic in this module. - -type SVGMapSource = 'string' | Document | SVGElement; -type GeoJSONMapSource = 'string' | GeoJSON | GeoJSONCompressed; -type MapInputObject = { - geoJSON?: GeoJSONMapSource; - geoJson?: GeoJSONMapSource; - svg?: SVGMapSource; - specialAreas?: GeoSpecialAreas; -}; - -export type MapRecord = GeoJSONMapRecord | SVGMapRecord; -export interface GeoJSONMapRecord { - type: 'geoJSON'; - source: GeoJSONMapSource; - specialAreas: GeoSpecialAreas; - geoJSON: GeoJSON | GeoJSONCompressed; -} -export interface SVGMapRecord { - type: 'svg'; - source: SVGMapSource; - specialAreas: GeoSpecialAreas; - svgXML: ReturnType<typeof parseXML>; -} - - -const storage = createHashMap<MapRecord[]>(); - - -export default { - - /** - * Compatible with previous `echarts.registerMap`. - * @usage - * ```js - * $.get('USA.json', function (geoJson) { - * echarts.registerMap('USA', geoJson); - * // Or - * echarts.registerMap('USA', { - * geoJson: geoJson, - * specialAreas: {} - * }) - * }); - * - * $.get('airport.svg', function (svg) { - * echarts.registerMap('airport', { - * svg: svg - * } - * }); - * - * echarts.registerMap('eu', [ - * {svg: eu-topographic.svg}, - * {geoJSON: eu.json} - * ]) - * ``` - */ - registerMap: function ( - mapName: string, - rawDef: MapInputObject | MapRecord[] | GeoJSONMapSource, - rawSpecialAreas?: GeoSpecialAreas - ): MapRecord[] { - - let records: MapRecord[]; - - if (isArray(rawDef)) { - records = rawDef as MapRecord[]; - } - else if ((rawDef as MapInputObject).svg) { - records = [{ - type: 'svg', - source: (rawDef as MapInputObject).svg, - specialAreas: (rawDef as MapInputObject).specialAreas - } as SVGMapRecord]; - } - else { - // Backward compatibility. - const geoSource = (rawDef as MapInputObject).geoJson - || (rawDef as MapInputObject).geoJSON; - if (geoSource && !(rawDef as GeoJSON).features) { - rawSpecialAreas = (rawDef as MapInputObject).specialAreas; - rawDef = geoSource; - } - records = [{ - type: 'geoJSON', - source: rawDef as GeoJSONMapSource, - specialAreas: rawSpecialAreas - } as GeoJSONMapRecord]; - } - - each(records, function (record) { - let type = record.type; - (type as any) === 'geoJson' && (type = record.type = 'geoJSON'); - - const parse = parsers[type]; - - if (__DEV__) { - assert(parse, 'Illegal map type: ' + type); - } - - parse(record); - }); - - return storage.set(mapName, records); - }, - - retrieveMap: function (mapName: string): MapRecord[] { - return storage.get(mapName); - } - -}; - -const parsers: Dictionary<(record: MapRecord) => void> = { - - geoJSON: function (record: GeoJSONMapRecord): void { - const source = record.source; - record.geoJSON = !isString(source) - ? source - : (typeof JSON !== 'undefined' && JSON.parse) - ? JSON.parse(source) - : (new Function('return (' + source + ');'))(); - }, - - // 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. - svg: function (record: SVGMapRecord): void { - record.svgXML = parseXML(record.source as SVGMapSource); - } - -}; diff --git a/src/core/echarts.ts b/src/core/echarts.ts index 20cf0f2..6bf60e8 100644 --- a/src/core/echarts.ts +++ b/src/core/echarts.ts @@ -68,7 +68,6 @@ import loadingDefault from '../loading/default'; import Scheduler from './Scheduler'; import lightTheme from '../theme/light'; import darkTheme from '../theme/dark'; -import mapDataStorage from '../coord/geo/mapDataStorage'; import {CoordinateSystemMaster, CoordinateSystemCreator, CoordinateSystemHostModel} from '../coord/CoordinateSystem'; import { parseClassType } from '../util/clazz'; import {ECEventProcessor} from '../util/ECEventProcessor'; @@ -105,6 +104,7 @@ import decal from '../visual/decal'; import type {MorphDividingMethod} from 'zrender/src/tool/morphPath'; import CanvasPainter from 'zrender/src/canvas/Painter'; import SVGPainter from 'zrender/src/svg/Painter'; +import geoSourceManager from '../coord/geo/geoSourceManager'; declare let global: any; @@ -2831,26 +2831,19 @@ export function setCanvasCreator(creator: () => HTMLCanvasElement): void { } /** - * The parameters and usage: see `mapDataStorage.registerMap`. + * The parameters and usage: see `geoSourceManager.registerMap`. * Compatible with previous `echarts.registerMap`. */ export function registerMap( - mapName: Parameters<typeof mapDataStorage.registerMap>[0], - geoJson: Parameters<typeof mapDataStorage.registerMap>[1], - specialAreas?: Parameters<typeof mapDataStorage.registerMap>[2] + mapName: Parameters<typeof geoSourceManager.registerMap>[0], + geoJson: Parameters<typeof geoSourceManager.registerMap>[1], + specialAreas?: Parameters<typeof geoSourceManager.registerMap>[2] ): void { - mapDataStorage.registerMap(mapName, geoJson, specialAreas); + geoSourceManager.registerMap(mapName, geoJson, specialAreas); } export function getMap(mapName: string) { - // For backward compatibility, only return the first one. - const records = mapDataStorage.retrieveMap(mapName); - // FIXME support SVG, where return not only records[0]. - return records && records[0] && { - // @ts-ignore - geoJson: records[0].geoJSON, - specialAreas: records[0].specialAreas - }; + return geoSourceManager.getMapForUser(mapName); } export const registerTransform = registerExternalTransform; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
