You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2021/03/09 17:58:21 UTC

[echarts] 03/06: 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.

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 <su...@gmail.com>
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: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org