echarts GeoSVGResource 源码

  • 2022-10-20
  • 浏览 (449)

echarts GeoSVGResource 代码

文件路径:/src/coord/geo/GeoSVGResource.ts

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { parseSVG, makeViewBoxTransform, SVGNodeTagLower, SVGParserResultNamedItem } from 'zrender/src/tool/parseSVG';
import Group from 'zrender/src/graphic/Group';
import Rect from 'zrender/src/graphic/shape/Rect';
import {assert, createHashMap, each, HashMap} from 'zrender/src/core/util';
import BoundingRect from 'zrender/src/core/BoundingRect';
import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput } from './geoTypes';
import { parseXML } from 'zrender/src/tool/parseXML';
import { GeoSVGRegion } from './Region';
import Element from 'zrender/src/Element';

export interface GeoSVGGraphicRecord {
    root: Group;
    boundingRect: BoundingRect;
    named: SVGParserResultNamedItem[];
}

/**
 * "region available" means that: enable users to set attribute `name="xxx"` on those tags
 * to make it be a region.
 * 1. region styles and its label styles can be defined in echarts opton:
 * ```js
 * geo: {
 *     regions: [{
 *         name: 'xxx',
 *         itemStyle: { ... },
 *         label: { ... }
 *     }, {
 *         ...
 *     },
 *     ...]
 * };
 * ```
 * 2. name can be duplicated in different SVG tag. All of the tags with the same name share
 * a region option. For exampel if there are two <path> representing two lung lobes. They have
 * no common parents but both of them need to display label "lung" inside.
 */
const REGION_AVAILABLE_SVG_TAG_MAP = createHashMap<number, SVGNodeTagLower>([
    'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path',
    // <text> <tspan> are also enabled because some SVG might paint text itself,
    // but still need to trigger events or tooltip.
    'text', 'tspan',
    // <g> is also enabled because this case: if multiple tags share one name
    // and need label displayed, every tags will display the name, which is not
    // expected. So we can put them into a <g name="xxx">. Thereby only one label
    // displayed and located based on the bounding rect of the <g>.
    'g'
]);

export class GeoSVGResource implements GeoResource {

    readonly type = 'geoSVG';
    private _mapName: string;
    private _parsedXML: SVGElement;

    private _firstGraphic: GeoSVGGraphicRecord;
    private _boundingRect: BoundingRect;
    private _regions: GeoSVGRegion[];
    // Key: region.name
    private _regionsMap: HashMap<GeoSVGRegion>;

    // All used graphics. key: hostKey, value: root
    private _usedGraphicMap: HashMap<GeoSVGGraphicRecord> = createHashMap();
    // All unused graphics.
    private _freedGraphics: GeoSVGGraphicRecord[] = [];

    constructor(
        mapName: string,
        svg: GeoSVGSourceInput
    ) {
        this._mapName = mapName;

        // Only perform parse to XML object here, which might be time
        // consiming for large SVG.
        // Although convert XML to zrender element is also time consiming,
        // if we do it here, the clone of zrender elements has to be
        // required. So we do it once for each geo instance, util real
        // performance issues call for optimizing it.
        this._parsedXML = parseXML(svg);
    }

    load(/* nameMap: NameMap */) {
        // In the "load" stage, graphic need to be built to
        // get boundingRect for geo coordinate system.
        let firstGraphic = this._firstGraphic;

        // Create the return data structure only when first graphic created.
        // Because they will be used in geo coordinate system update stage,
        // and `regions` will be mounted at `geo` coordinate system,
        // in which there is no "view" info, so that it should better not to
        // make references to graphic elements.
        if (!firstGraphic) {
            firstGraphic = this._firstGraphic = this._buildGraphic(this._parsedXML);

            this._freedGraphics.push(firstGraphic);

            this._boundingRect = this._firstGraphic.boundingRect.clone();

            // PENDING: `nameMap` will not be supported until some real requirement come.
            // if (nameMap) {
            //     named = applyNameMap(named, nameMap);
            // }

            const { regions, regionsMap } = createRegions(firstGraphic.named);
            this._regions = regions;
            this._regionsMap = regionsMap;
        }

        return {
            boundingRect: this._boundingRect,
            regions: this._regions,
            regionsMap: this._regionsMap
        };
    }

    private _buildGraphic(
        svgXML: SVGElement
    ): GeoSVGGraphicRecord {
        let result;
        let rootFromParse;

        try {
            result = svgXML && parseSVG(svgXML, {
                ignoreViewBox: true,
                ignoreRootClip: true
            }) || {};
            rootFromParse = result.root;
            assert(rootFromParse != null);
        }
        catch (e) {
            throw new Error('Invalid svg format\n' + e.message);
        }

        // Note: we keep the covenant that the root has no transform. So always add an extra root.
        const root = new Group();
        root.add(rootFromParse);
        (root as GeoSVGGraphicRoot).isGeoSVGGraphicRoot = true;

        // [THE_RULE_OF_VIEWPORT_AND_VIEWBOX]
        //
        // Consider: `<svg width="..." height="..." viewBox="...">`
        // - the `width/height` we call it `svgWidth/svgHeight` for short.
        // - `(0, 0, svgWidth, svgHeight)` defines the viewport of the SVG, or say,
        //   "viewport boundingRect", or `boundingRect` for short.
        // - `viewBox` defines the transform from the real content ot the viewport.
        //   `viewBox` has the same unit as the content of SVG.
        //   If `viewBox` exists, a transform is defined, so the unit of `svgWidth/svgHeight` become
        //   different from the content of SVG. Otherwise, they are the same.
        //
        // If both `svgWidth/svgHeight/viewBox` are specified in a SVG file, the transform rule will be:
        // 0. `boundingRect` is `(0, 0, svgWidth, svgHeight)`. Set it to Geo['_rect'] (View['_rect']).
        // 1. Make a transform from `viewBox` to `boundingRect`.
        //    Note: only suport `preserveAspectRatio 'xMidYMid'` here. That is, this transform will preserve
        //    the aspect ratio.
        // 2. Make a transform from boundingRect to Geo['_viewRect'] (View['_viewRect'])
        //    (`Geo`/`View` will do this job).
        //    Note: this transform might not preserve aspect radio, which depending on how users specify
        //    viewRect in echarts option (e.g., `geo.left/top/width/height` will not preserve aspect ratio,
        //    but `geo.layoutCenter/layoutSize` will preserve aspect ratio).
        //
        // If `svgWidth/svgHeight` not specified, we use `viewBox` as the `boundingRect` to make the SVG
        // layout look good.
        //
        // If neither `svgWidth/svgHeight` nor `viewBox` are not specified, we calculate the boundingRect
        // of the SVG content and use them to make SVG layout look good.

        const svgWidth = result.width;
        const svgHeight = result.height;
        const viewBoxRect = result.viewBoxRect;

        let boundingRect = this._boundingRect;
        if (!boundingRect) {
            let bRectX;
            let bRectY;
            let bRectWidth;
            let bRectHeight;

            if (svgWidth != null) {
                bRectX = 0;
                bRectWidth = svgWidth;
            }
            else if (viewBoxRect) {
                bRectX = viewBoxRect.x;
                bRectWidth = viewBoxRect.width;
            }

            if (svgHeight != null) {
                bRectY = 0;
                bRectHeight = svgHeight;
            }
            else if (viewBoxRect) {
                bRectY = viewBoxRect.y;
                bRectHeight = viewBoxRect.height;
            }

            // If both viewBox and svgWidth/svgHeight not specified,
            // we have to determine how to layout those element to make them look good.
            if (bRectX == null || bRectY == null) {
                const calculatedBoundingRect = rootFromParse.getBoundingRect();
                if (bRectX == null) {
                    bRectX = calculatedBoundingRect.x;
                    bRectWidth = calculatedBoundingRect.width;
                }
                if (bRectY == null) {
                    bRectY = calculatedBoundingRect.y;
                    bRectHeight = calculatedBoundingRect.height;
                }
            }

            boundingRect = this._boundingRect = new BoundingRect(bRectX, bRectY, bRectWidth, bRectHeight);
        }

        if (viewBoxRect) {
            const viewBoxTransform = makeViewBoxTransform(viewBoxRect, boundingRect);
            // Only support `preserveAspectRatio 'xMidYMid'`
            rootFromParse.scaleX = rootFromParse.scaleY = viewBoxTransform.scale;
            rootFromParse.x = viewBoxTransform.x;
            rootFromParse.y = viewBoxTransform.y;
        }

        // SVG needs to clip based on `viewBox`. And some SVG files really rely on this feature.
        // They do not strictly confine all of the content inside a display rect, but deliberately
        // use a `viewBox` to define a displayable rect.
        // PENDING:
        // The drawback of the `setClipPath` here is: the region label (genereted by echarts) near the
        // edge might also be clipped, because region labels are put as `textContent` of the SVG path.
        root.setClipPath(new Rect({
            shape: boundingRect.plain()
        }));

        const named = [] as GeoSVGGraphicRecord['named'];
        each(result.named, namedItem => {
            if (REGION_AVAILABLE_SVG_TAG_MAP.get(namedItem.svgNodeTagLower) != null) {
                named.push(namedItem);
                setSilent(namedItem.el);
            }
        });

        return { root, boundingRect, named };
    }

    /**
     * Consider:
     * (1) One graphic element can not be shared by different `geoView` running simultaneously.
     *     Notice, also need to consider multiple echarts instances share a `mapRecord`.
     * (2) Converting SVG to graphic elements is time consuming.
     * (3) In the current architecture, `load` should be called frequently to get boundingRect,
     *     and it is called without view info.
     * So we maintain graphic elements in this module, and enables `view` to use/return these
     * graphics from/to the pool with it's uid.
     */
    useGraphic(hostKey: string /* , nameMap: NameMap */): GeoSVGGraphicRecord {
        const usedRootMap = this._usedGraphicMap;

        let svgGraphic = usedRootMap.get(hostKey);
        if (svgGraphic) {
            return svgGraphic;
        }

        svgGraphic = this._freedGraphics.pop()
            // use the first boundingRect to avoid duplicated boundingRect calculation.
            || this._buildGraphic(this._parsedXML);

        usedRootMap.set(hostKey, svgGraphic);

        // PENDING: `nameMap` will not be supported until some real requirement come.
        // `nameMap` can only be obtained from echarts option.
        // The original `named` must not be modified.
        // if (nameMap) {
        //     svgGraphic = extend({}, svgGraphic);
        //     svgGraphic.named = applyNameMap(svgGraphic.named, nameMap);
        // }

        return svgGraphic;
    }

    freeGraphic(hostKey: string): void {
        const usedRootMap = this._usedGraphicMap;

        const svgGraphic = usedRootMap.get(hostKey);
        if (svgGraphic) {
            usedRootMap.removeKey(hostKey);
            this._freedGraphics.push(svgGraphic);
        }
    }

}


function setSilent(el: Element): void {
    // Only named element has silent: false, other elements should
    // act as background and has no user interaction.
    el.silent = false;
    // text|tspan will be converted to group.
    if (el.isGroup) {
        el.traverse(child => {
            child.silent = false;
        });
    }
}

function createRegions(
    named: SVGParserResultNamedItem[]
): {
    regions: GeoSVGRegion[];
    regionsMap: HashMap<GeoSVGRegion>;
} {

    const regions: GeoSVGRegion[] = [];
    const regionsMap = createHashMap<GeoSVGRegion>();

    // Create resions only for the first graphic.
    each(named, namedItem => {
        // Region has feature to calculate center for tooltip or other features.
        // If there is a <g name="xxx">, the center should be the center of the
        // bounding rect of the g.
        if (namedItem.namedFrom != null) {
            return;
        }

        const region = new GeoSVGRegion(namedItem.name, namedItem.el);
        // PENDING: if `nameMap` supported, this region can not be mounted on
        // `this`, but can only be created each time `load()` called.
        regions.push(region);
        // PENDING: if multiple tag named with the same name, only one will be
        // found by `_regionsMap`. `_regionsMap` is used to find a coordinate
        // by name. We use `region.getCenter()` as the coordinate.
        regionsMap.set(namedItem.name, region);
    });

    return { regions, regionsMap };
}


// PENDING: `nameMap` will not be supported until some real requirement come.
// /**
//  * Use the alias in geoNameMap.
//  * The input `named` must not be modified.
//  */
// function applyNameMap(
//     named: GeoSVGGraphicRecord['named'],
//     nameMap: NameMap
// ): GeoSVGGraphicRecord['named'] {
//     const result = [] as GeoSVGGraphicRecord['named'];
//     for (let i = 0; i < named.length; i++) {
//         let regionGraphic = named[i];
//         const name = regionGraphic.name;
//         if (nameMap && nameMap.hasOwnProperty(name)) {
//             regionGraphic = extend({}, regionGraphic);
//             regionGraphic.name = name;
//         }
//         result.push(regionGraphic);
//     }
//     return result;
// }

相关信息

echarts 源码目录

相关文章

echarts Geo 源码

echarts GeoJSONResource 源码

echarts GeoModel 源码

echarts Region 源码

echarts geoCreator 源码

echarts geoSourceManager 源码

echarts geoTypes 源码

echarts parseGeoJson 源码

echarts prepareCustom 源码

0  赞