MapDraw.ts 20.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
* 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.
*/

S
sushuang 已提交
20
import * as zrUtil from 'zrender/src/core/util';
S
sushuang 已提交
21 22 23 24
import RoamController from './RoamController';
import * as roamHelper from '../../component/helper/roamHelper';
import {onIrrelevantElement} from '../../component/helper/cursorHelper';
import * as graphic from '../../util/graphic';
P
pissang 已提交
25
import { enableHoverEmphasis, DISPLAY_STATES } from '../../util/states';
S
sushuang 已提交
26 27
import geoSourceManager from '../../coord/geo/geoSourceManager';
import {getUID} from '../../util/component';
28
import ExtensionAPI from '../../core/ExtensionAPI';
29 30 31
import GeoModel, { GeoCommonOptionMixin, GeoItemStyleOption } from '../../coord/geo/GeoModel';
import MapSeries from '../../chart/map/MapSeries';
import GlobalModel from '../../model/Global';
P
pissang 已提交
32
import { Payload, ECElement } from '../../util/types';
33 34 35 36
import GeoView from '../geo/GeoView';
import MapView from '../../chart/map/MapView';
import Geo from '../../coord/geo/Geo';
import Model from '../../model/Model';
P
pissang 已提交
37
import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
38
import { getECData } from '../../util/innerStore';
P
pissang 已提交
39
import { createOrUpdatePatternFromDecal } from '../../util/decal';
1
100pah 已提交
40 41
import { ViewCoordSysTransformInfoPart } from '../../coord/View';
import { GeoSVGResource } from '../../coord/geo/GeoSVGResource';
42 43 44
import Displayable from 'zrender/src/graphic/Displayable';
import Element, { ElementTextConfig } from 'zrender/src/Element';
import List from '../../data/List';
1
100pah 已提交
45
import { GeoJSONRegion } from '../../coord/geo/Region';
46 47 48 49


interface RegionsGroup extends graphic.Group {
}
S
sushuang 已提交
50

51 52 53 54 55 56 57 58 59 60
interface ViewBuildContext {
    api: ExtensionAPI;
    geo: Geo;
    mapOrGeoModel: GeoModel | MapSeries;
    data: List;
    isVisualEncodedByVisualMap: boolean;
    isGeo: boolean;
    transformInfoRaw: ViewCoordSysTransformInfoPart;
}

61
function getFixedItemStyle(model: Model<GeoItemStyleOption>) {
62 63
    const itemStyle = model.getItemStyle();
    const areaColor = model.get('areaColor');
S
sushuang 已提交
64 65 66 67 68

    // If user want the color not to be changed when hover,
    // they should both set areaColor and color to be null.
    if (areaColor != null) {
        itemStyle.fill = areaColor;
L
lang 已提交
69 70
    }

S
sushuang 已提交
71 72
    return itemStyle;
}
73
class MapDraw {
74

75
    private uid: string;
S
sushuang 已提交
76

77
    private _controller: RoamController;
78

79
    private _controllerHost: {
P
pissang 已提交
80
        target: graphic.Group;
81 82 83
        zoom?: number;
        zoomLimit?: GeoCommonOptionMixin['scaleLimit'];
    };
L
lang 已提交
84

85
    readonly group: graphic.Group;
L
lang 已提交
86 87


S
sushuang 已提交
88 89 90 91 92
    /**
     * This flag is used to make sure that only one among
     * `pan`, `zoom`, `click` can occurs, otherwise 'selected'
     * action may be triggered when `pan`, which is unexpected.
     */
93
    private _mouseDownFlag: boolean;
S
sushuang 已提交
94

95 96
    private _regionsGroup: RegionsGroup;

1
100pah 已提交
97 98
    private _svgMapName: string;

1
100pah 已提交
99
    private _svgGroup: graphic.Group;
S
sushuang 已提交
100

1
100pah 已提交
101
    private _svgRegionElements: Displayable[];
102

103

P
pissang 已提交
104
    constructor(api: ExtensionAPI) {
105
        const group = new graphic.Group();
106 107
        this.uid = getUID('ec_map_draw');
        this._controller = new RoamController(api.getZr());
P
pissang 已提交
108
        this._controllerHost = { target: group };
109
        this.group = group;
L
lang 已提交
110

111
        group.add(this._regionsGroup = new graphic.Group() as RegionsGroup);
1
100pah 已提交
112
        group.add(this._svgGroup = new graphic.Group());
113
    }
L
lang 已提交
114

115 116 117 118
    draw(
        mapOrGeoModel: GeoModel | MapSeries,
        ecModel: GlobalModel,
        api: ExtensionAPI,
1
100pah 已提交
119 120
        fromView: MapView | GeoView,
        payload: Payload
121
    ): void {
L
lang 已提交
122

123
        const isGeo = mapOrGeoModel.mainType === 'geo';
L
lang 已提交
124

S
sushuang 已提交
125 126
        // Map series has data. GEO model that controlled by map series
        // will be assigned with map data. Other GEO model has no data.
127
        let data = (mapOrGeoModel as MapSeries).getData && (mapOrGeoModel as MapSeries).getData();
128
        isGeo && ecModel.eachComponent({mainType: 'series', subType: 'map'}, function (mapSeries: MapSeries) {
S
sushuang 已提交
129 130
            if (!data && mapSeries.getHostGeoModel() === mapOrGeoModel) {
                data = mapSeries.getData();
L
lang 已提交
131
            }
S
sushuang 已提交
132 133
        });

134
        const geo = mapOrGeoModel.coordinateSystem;
S
sushuang 已提交
135

136 137
        const regionsGroup = this._regionsGroup;
        const group = this.group;
L
lang 已提交
138

139
        const transformInfo = geo.getTransformInfo();
140 141
        const transformInfoRaw = transformInfo.raw;
        const transformInfoRoam = transformInfo.roam;
142 143 144

        // No animation when first draw or in action
        const isFirstDraw = !regionsGroup.childAt(0) || payload;
145

146
        if (isFirstDraw) {
147 148 149 150
            group.x = transformInfoRoam.x;
            group.y = transformInfoRoam.y;
            group.scaleX = transformInfoRoam.scaleX;
            group.scaleY = transformInfoRoam.scaleY;
151 152 153
            group.dirty();
        }
        else {
154
            graphic.updateProps(group, transformInfoRoam, mapOrGeoModel);
155
        }
L
lang 已提交
156

P
pissang 已提交
157 158 159
        const isVisualEncodedByVisualMap = data
            && data.getVisual('visualMeta')
            && data.getVisual('visualMeta').length > 0;
160

161 162 163 164 165 166 167 168 169 170
        const viewBuildCtx = {
            api,
            geo,
            mapOrGeoModel,
            data,
            isVisualEncodedByVisualMap,
            isGeo,
            transformInfoRaw
        };

1
100pah 已提交
171 172 173 174 175 176
        if (geo.resourceType === 'geoJSON') {
            this._buildGeoJSON(viewBuildCtx);
        }
        else if (geo.resourceType === 'geoSVG') {
            this._buildSVG(viewBuildCtx);
        }
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

        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();
1
100pah 已提交
196

197
        // Only when the resource is GeoJSON, there is `geo.regions`.
1
100pah 已提交
198
        zrUtil.each(viewBuildCtx.geo.regions, function (region: GeoJSONRegion) {
1
100pah 已提交
199

S
sushuang 已提交
200 201 202 203 204
            // Consider in GeoJson properties.name may be duplicated, for example,
            // there is multiple region named "United Kindom" or "France" (so many
            // colonies). And it is not appropriate to merge them in geo, which
            // will make them share the same label and bring trouble in label
            // location calculation.
205
            const regionGroup = nameMap.get(region.name)
206
                || nameMap.set(region.name, new graphic.Group() as RegionsGroup);
S
sushuang 已提交
207

208
            const compoundPath = new graphic.CompoundPath({
209
                segmentIgnoreThreshold: 1,
S
sushuang 已提交
210 211 212 213 214 215 216 217 218 219
                shape: {
                    paths: []
                }
            });
            regionGroup.add(compoundPath);

            zrUtil.each(region.geometries, function (geometry) {
                if (geometry.type !== 'polygon') {
                    return;
                }
220
                const points = [];
1
100pah 已提交
221
                for (let i = 0; i < geometry.exterior.length; ++i) {
222
                    points.push(transformPoint(geometry.exterior[i]));
223
                }
S
sushuang 已提交
224
                compoundPath.shape.paths.push(new graphic.Polygon({
225
                    segmentIgnoreThreshold: 1,
S
sushuang 已提交
226
                    shape: {
227
                        points: points
L
lang 已提交
228
                    }
S
sushuang 已提交
229 230
                }));

1
100pah 已提交
231
                for (let i = 0; i < (geometry.interiors ? geometry.interiors.length : 0); ++i) {
232 233
                    const interior = geometry.interiors[i];
                    const points = [];
234
                    for (let j = 0; j < interior.length; ++j) {
235 236
                        points.push(transformPoint(interior[j]));
                    }
L
lang 已提交
237
                    compoundPath.shape.paths.push(new graphic.Polygon({
238
                        segmentIgnoreThreshold: 1,
L
lang 已提交
239
                        shape: {
240
                            points: points
L
lang 已提交
241
                        }
L
lang 已提交
242
                    }));
S
sushuang 已提交
243 244
                }
            });
L
lang 已提交
245

1
100pah 已提交
246
            const centerPt = transformPoint(region.getCenter());
S
sushuang 已提交
247

248 249 250
            this._resetSingleRegionGraphic(
                viewBuildCtx, compoundPath, regionGroup, region.name, centerPt, null
            );
S
sushuang 已提交
251

252
            regionsGroup.add(regionGroup);
P
pissang 已提交
253

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        }, 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);
        }
P
pah100 已提交
270

1
100pah 已提交
271
        zrUtil.each(this._svgRegionElements, function (el: Displayable) {
272 273 274 275
            // Note that we also allow different elements have the same name.
            // For example, a glyph of a city and the label of the city have
            // the same name and their tooltip info can be defined in a single
            // region option.
276
            this._resetSingleRegionGraphic(
1
100pah 已提交
277
                viewBuildCtx, el, el, el.name, [0, 0], 'inside'
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
            );
        }, 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;
S
sushuang 已提交
327
            }
328 329
            if (decal) {
                itemStyle.decal = createOrUpdatePatternFromDecal(decal, viewBuildCtx.api);
S
sushuang 已提交
330
            }
331 332 333 334 335
        }

        displayable.setStyle(itemStyle);
        displayable.style.strokeNoScale = true;
        displayable.culling = true;
L
lang 已提交
336

337 338 339 340 341 342 343 344
        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];
345
            // @ts-ignore FIXME:TS fix the "compatible with each other"?
346 347 348 349 350 351 352
            if (regionModel.get(
                stateName === 'normal' ? ['label', 'show'] : [stateName, 'label', 'show']
            )) {
                showLabel = true;
                break;
            }
        }
353

354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
        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;
            }
L
lang 已提交
372

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
            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);
        }
1
100pah 已提交
410 411 412 413 414
        // series-map will not trigger "geoselectchange" no matter it is
        // based on a declared geo component. Becuause series-map will
        // trigger "selectchange". If it trigger both the two events,
        // If users call `chart.dispatchAction({type: 'toggleSelect'})`,
        // it not easy to also fire event "geoselectchanged".
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
        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'));
L
lang 已提交
430

431
    }
432

433
    remove(): void {
S
sushuang 已提交
434
        this._regionsGroup.removeAll();
1
100pah 已提交
435
        this._svgGroup.removeAll();
S
sushuang 已提交
436
        this._controller.dispose();
437
        this._freeSVG();
P
pissang 已提交
438
        this._controllerHost = null;
439
    }
440

441 442
    private _svgResourceChanged(mapName: string): boolean {
        return this._svgMapName !== mapName;
1
100pah 已提交
443
    }
S
sushuang 已提交
444

445
    private _useSVG(mapName: string): void {
1
100pah 已提交
446
        const resource = geoSourceManager.getGeoResource(mapName);
1
100pah 已提交
447
        if (resource && resource.type === 'geoSVG') {
1
100pah 已提交
448 449
            const svgGraphic = (resource as GeoSVGResource).useGraphic(this.uid);
            this._svgGroup.add(svgGraphic.root);
1
100pah 已提交
450
            this._svgRegionElements = svgGraphic.regionElements;
451
            this._svgMapName = mapName;
1
100pah 已提交
452 453 454
        }
    }

455 456
    private _freeSVG(): void {
        const mapName = this._svgMapName;
1
100pah 已提交
457 458 459 460
        if (mapName == null) {
            return;
        }
        const resource = geoSourceManager.getGeoResource(mapName);
1
100pah 已提交
461
        if (resource && resource.type === 'geoSVG') {
1
100pah 已提交
462 463
            (resource as GeoSVGResource).freeGraphic(this.uid);
        }
1
100pah 已提交
464
        this._svgRegionElements = null;
465 466
        this._svgGroup.removeAll();
        this._svgMapName = null;
467
    }
S
sushuang 已提交
468

469
    private _updateController(
1
100pah 已提交
470
        this: MapDraw, mapOrGeoModel: GeoModel | MapSeries, ecModel: GlobalModel, api: ExtensionAPI
471
    ): void {
472 473 474
        const geo = mapOrGeoModel.coordinateSystem;
        const controller = this._controller;
        const controllerHost = this._controllerHost;
475

476
        // @ts-ignore FIXME:TS
S
sushuang 已提交
477 478
        controllerHost.zoomLimit = mapOrGeoModel.get('scaleLimit');
        controllerHost.zoom = geo.getZoom();
P
pah100 已提交
479

S
sushuang 已提交
480
        // roamType is will be set default true if it is null
481
        // @ts-ignore FIXME:TS
S
sushuang 已提交
482
        controller.enable(mapOrGeoModel.get('roam') || false);
483
        const mainType = mapOrGeoModel.mainType;
484

485
        function makeActionBase(): Payload {
486
            const action = {
S
sushuang 已提交
487 488
                type: 'geoRoam',
                componentType: mainType
489
            } as Payload;
S
sushuang 已提交
490 491 492
            action[mainType + 'Id'] = mapOrGeoModel.id;
            return action;
        }
493

1
100pah 已提交
494
        controller.off('pan').on('pan', function (e) {
S
sushuang 已提交
495
            this._mouseDownFlag = false;
P
pah100 已提交
496

497
            roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy);
498

S
sushuang 已提交
499
            api.dispatchAction(zrUtil.extend(makeActionBase(), {
500 501
                dx: e.dx,
                dy: e.dy
S
sushuang 已提交
502 503
            }));
        }, this);
L
lang 已提交
504

1
100pah 已提交
505
        controller.off('zoom').on('zoom', function (e) {
S
sushuang 已提交
506 507
            this._mouseDownFlag = false;

508
            roamHelper.updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY);
S
sushuang 已提交
509 510

            api.dispatchAction(zrUtil.extend(makeActionBase(), {
511 512 513
                zoom: e.scale,
                originX: e.originX,
                originY: e.originY
S
sushuang 已提交
514 515 516 517 518 519
            }));

        }, this);

        controller.setPointerChecker(function (e, x, y) {
            return geo.getViewRectAfterRoam().contain(x, y)
S
sushuang 已提交
520
                && !onIrrelevantElement(e, api, mapOrGeoModel);
S
sushuang 已提交
521 522
        });
    }
523 524 525 526 527 528 529

    private _updateMapSelectHandler(
        mapOrGeoModel: GeoModel | MapSeries,
        regionsGroup: RegionsGroup,
        api: ExtensionAPI,
        fromView: MapView | GeoView
    ): void {
530
        const mapDraw = this;
531 532

        regionsGroup.off('mousedown');
1
100pah 已提交
533
        regionsGroup.off('click');
534 535 536 537 538 539 540 541 542

        // @ts-ignore FIXME:TS resolve type conflict
        if (mapOrGeoModel.get('selectedMode')) {

            regionsGroup.on('mousedown', function () {
                mapDraw._mouseDownFlag = true;
            });

            regionsGroup.on('click', function (e) {
543
                if (!mapDraw._mouseDownFlag) {
544 545
                    return;
                }
546
                mapDraw._mouseDownFlag = false;
547 548 549 550
            });
        }
    }

S
sushuang 已提交
551
};
L
lang 已提交
552

1
100pah 已提交
553 554 555 556 557 558 559 560 561 562 563 564 565
function labelTextAfterUpdate(this: graphic.Text) {
    // Make the label text do not scale but perform translate
    // based on its host el.
    const m = this.transform;
    const scaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
    const scaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]);

    m[0] /= scaleX;
    m[1] /= scaleX;
    m[2] /= scaleY;
    m[3] /= scaleY;
}

566
export default MapDraw;