/* * 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. */ var zrUtil = require("zrender/lib/core/util"); var RoamController = require("./RoamController"); var roamHelper = require("../../component/helper/roamHelper"); var _cursorHelper = require("../../component/helper/cursorHelper"); var onIrrelevantElement = _cursorHelper.onIrrelevantElement; var graphic = require("../../util/graphic"); var geoSourceManager = require("../../coord/geo/geoSourceManager"); var _component = require("../../util/component"); var getUID = _component.getUID; var Transformable = require("zrender/lib/mixin/Transformable"); /* * 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. */ function getFixedItemStyle(model) { var itemStyle = model.getItemStyle(); var areaColor = model.get('areaColor'); // 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; } return itemStyle; } function updateMapSelectHandler(mapDraw, mapOrGeoModel, regionsGroup, api, fromView) { regionsGroup.off('click'); regionsGroup.off('mousedown'); if (mapOrGeoModel.get('selectedMode')) { regionsGroup.on('mousedown', function () { mapDraw._mouseDownFlag = true; }); regionsGroup.on('click', function (e) { if (!mapDraw._mouseDownFlag) { return; } mapDraw._mouseDownFlag = false; var el = e.target; while (!el.__regions) { el = el.parent; } if (!el) { return; } var action = { type: (mapOrGeoModel.mainType === 'geo' ? 'geo' : 'map') + 'ToggleSelect', batch: zrUtil.map(el.__regions, function (region) { return { name: region.name, from: fromView.uid }; }) }; action[mapOrGeoModel.mainType + 'Id'] = mapOrGeoModel.id; api.dispatchAction(action); updateMapSelected(mapOrGeoModel, regionsGroup); }); } } function updateMapSelected(mapOrGeoModel, regionsGroup) { // FIXME regionsGroup.eachChild(function (otherRegionEl) { zrUtil.each(otherRegionEl.__regions, function (region) { otherRegionEl.trigger(mapOrGeoModel.isSelected(region.name) ? 'emphasis' : 'normal'); }); }); } /** * @alias module:echarts/component/helper/MapDraw * @param {module:echarts/ExtensionAPI} api * @param {boolean} updateGroup */ function MapDraw(api, updateGroup) { var group = new graphic.Group(); /** * @type {string} * @private */ this.uid = getUID('ec_map_draw'); /** * @type {module:echarts/component/helper/RoamController} * @private */ this._controller = new RoamController(api.getZr()); /** * @type {Object} {target, zoom, zoomLimit} * @private */ this._controllerHost = { target: updateGroup ? group : null }; /** * @type {module:zrender/container/Group} * @readOnly */ this.group = group; /** * @type {boolean} * @private */ this._updateGroup = updateGroup; /** * 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. * @type {booelan} */ this._mouseDownFlag; /** * @type {string} */ this._mapName; /** * @type {boolean} */ this._initialized; /** * @type {module:zrender/container/Group} */ group.add(this._regionsGroup = new graphic.Group()); /** * @type {module:zrender/container/Group} */ group.add(this._backgroundGroup = new graphic.Group()); } MapDraw.prototype = { constructor: MapDraw, draw: function (mapOrGeoModel, ecModel, api, fromView, payload) { var isGeo = mapOrGeoModel.mainType === 'geo'; // Map series has data. GEO model that controlled by map series // will be assigned with map data. Other GEO model has no data. var data = mapOrGeoModel.getData && mapOrGeoModel.getData(); isGeo && ecModel.eachComponent({ mainType: 'series', subType: 'map' }, function (mapSeries) { if (!data && mapSeries.getHostGeoModel() === mapOrGeoModel) { data = mapSeries.getData(); } }); var geo = mapOrGeoModel.coordinateSystem; this._updateBackground(geo); var regionsGroup = this._regionsGroup; var group = this.group; var transformInfo = geo.getTransformInfo(); // No animation when first draw or in action var isFirstDraw = !regionsGroup.childAt(0) || payload; var targetScale; if (isFirstDraw) { group.transform = transformInfo.roamTransform; group.decomposeTransform(); group.dirty(); } else { var target = new Transformable(); target.transform = transformInfo.roamTransform; target.decomposeTransform(); var props = { scale: target.scale, position: target.position }; targetScale = target.scale; graphic.updateProps(group, props, mapOrGeoModel); } var scale = transformInfo.rawScale; var position = transformInfo.rawPosition; regionsGroup.removeAll(); var itemStyleAccessPath = ['itemStyle']; var hoverItemStyleAccessPath = ['emphasis', 'itemStyle']; var labelAccessPath = ['label']; var hoverLabelAccessPath = ['emphasis', 'label']; var nameMap = zrUtil.createHashMap(); zrUtil.each(geo.regions, function (region) { // Consider in GeoJson properties.name may be duplicated, for example, // there is multiple region named "United Kindom" or "France" (so many // 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. var regionGroup = nameMap.get(region.name) || nameMap.set(region.name, new graphic.Group()); var compoundPath = new graphic.CompoundPath({ segmentIgnoreThreshold: 1, shape: { paths: [] } }); regionGroup.add(compoundPath); var regionModel = mapOrGeoModel.getRegionModel(region.name) || mapOrGeoModel; var itemStyleModel = regionModel.getModel(itemStyleAccessPath); var hoverItemStyleModel = regionModel.getModel(hoverItemStyleAccessPath); var itemStyle = getFixedItemStyle(itemStyleModel); var hoverItemStyle = getFixedItemStyle(hoverItemStyleModel); var labelModel = regionModel.getModel(labelAccessPath); var hoverLabelModel = regionModel.getModel(hoverLabelAccessPath); var dataIdx; // Use the itemStyle in data if has data if (data) { dataIdx = data.indexOfName(region.name); // Only visual color of each item will be used. It can be encoded by dataRange // But visual color of series is used in symbol drawing // // Visual color for each series is for the symbol draw var visualColor = data.getItemVisual(dataIdx, 'color', true); if (visualColor) { itemStyle.fill = visualColor; } } var transformPoint = function (point) { return [point[0] * scale[0] + position[0], point[1] * scale[1] + position[1]]; }; zrUtil.each(region.geometries, function (geometry) { if (geometry.type !== 'polygon') { return; } var points = []; for (var i = 0; i < geometry.exterior.length; ++i) { points.push(transformPoint(geometry.exterior[i])); } compoundPath.shape.paths.push(new graphic.Polygon({ segmentIgnoreThreshold: 1, shape: { points: points } })); for (var i = 0; i < (geometry.interiors ? geometry.interiors.length : 0); ++i) { var interior = geometry.interiors[i]; var points = []; for (var j = 0; j < interior.length; ++j) { points.push(transformPoint(interior[j])); } compoundPath.shape.paths.push(new graphic.Polygon({ segmentIgnoreThreshold: 1, shape: { points: points } })); } }); compoundPath.setStyle(itemStyle); compoundPath.style.strokeNoScale = true; compoundPath.culling = true; // Label var showLabel = labelModel.get('show'); var hoverShowLabel = hoverLabelModel.get('show'); var isDataNaN = data && isNaN(data.get(data.mapDimension('value'), dataIdx)); var 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 || hoverShowLabel) || itemLayout && itemLayout.showLabel) { var query = !isGeo ? dataIdx : region.name; var labelFetcher; // Consider dataIdx not found. if (!data || dataIdx >= 0) { labelFetcher = mapOrGeoModel; } var textEl = new graphic.Text({ position: transformPoint(region.center.slice()), // FIXME // label rotation is not support yet in geo or regions of series-map // that has no data. The rotation will be effected by this `scale`. // So needed to change to RectText? scale: [1 / group.scale[0], 1 / group.scale[1]], z2: 10, silent: true }); graphic.setLabelStyle(textEl.style, textEl.hoverStyle = {}, labelModel, hoverLabelModel, { labelFetcher: labelFetcher, labelDataIndex: query, defaultText: region.name, useInsideStyle: false }, { textAlign: 'center', textVerticalAlign: 'middle' }); if (!isFirstDraw) { // Text animation var textScale = [1 / targetScale[0], 1 / targetScale[1]]; graphic.updateProps(textEl, { scale: textScale }, mapOrGeoModel); } regionGroup.add(textEl); } // setItemGraphicEl, setHoverStyle after all polygons and labels // are added to the rigionGroup if (data) { data.setItemGraphicEl(dataIdx, regionGroup); } else { var regionModel = mapOrGeoModel.getRegionModel(region.name); // Package custom mouse event for geo component compoundPath.eventData = { componentType: 'geo', componentIndex: mapOrGeoModel.componentIndex, geoIndex: mapOrGeoModel.componentIndex, name: region.name, region: regionModel && regionModel.option || {} }; } var groupRegions = regionGroup.__regions || (regionGroup.__regions = []); groupRegions.push(region); regionGroup.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode'); graphic.setHoverStyle(regionGroup, hoverItemStyle); regionsGroup.add(regionGroup); }); this._updateController(mapOrGeoModel, ecModel, api); updateMapSelectHandler(this, mapOrGeoModel, regionsGroup, api, fromView); updateMapSelected(mapOrGeoModel, regionsGroup); }, remove: function () { this._regionsGroup.removeAll(); this._backgroundGroup.removeAll(); this._controller.dispose(); this._mapName && geoSourceManager.removeGraphic(this._mapName, this.uid); this._mapName = null; this._controllerHost = {}; }, _updateBackground: function (geo) { var mapName = geo.map; if (this._mapName !== mapName) { zrUtil.each(geoSourceManager.makeGraphic(mapName, this.uid), function (root) { this._backgroundGroup.add(root); }, this); } this._mapName = mapName; }, _updateController: function (mapOrGeoModel, ecModel, api) { var geo = mapOrGeoModel.coordinateSystem; var controller = this._controller; var controllerHost = this._controllerHost; controllerHost.zoomLimit = mapOrGeoModel.get('scaleLimit'); controllerHost.zoom = geo.getZoom(); // roamType is will be set default true if it is null controller.enable(mapOrGeoModel.get('roam') || false); var mainType = mapOrGeoModel.mainType; function makeActionBase() { var action = { type: 'geoRoam', componentType: mainType }; action[mainType + 'Id'] = mapOrGeoModel.id; return action; } controller.off('pan').on('pan', function (e) { this._mouseDownFlag = false; roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy); api.dispatchAction(zrUtil.extend(makeActionBase(), { dx: e.dx, dy: e.dy })); }, this); controller.off('zoom').on('zoom', function (e) { this._mouseDownFlag = false; roamHelper.updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY); api.dispatchAction(zrUtil.extend(makeActionBase(), { zoom: e.scale, originX: e.originX, originY: e.originY })); if (this._updateGroup) { var scale = this.group.scale; this._regionsGroup.traverse(function (el) { if (el.type === 'text') { el.attr('scale', [1 / scale[0], 1 / scale[1]]); } }); } }, this); controller.setPointerChecker(function (e, x, y) { return geo.getViewRectAfterRoam().contain(x, y) && !onIrrelevantElement(e, api, mapOrGeoModel); }); } }; var _default = MapDraw; module.exports = _default;