/* * 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, indexOf, curry, assert, map, createHashMap } from 'zrender/src/core/util'; import * as graphic from '../../util/graphic'; import * as brushHelper from './brushHelper'; import { BrushPanelConfig, BrushControllerEvents, BrushType, BrushAreaRange, BrushDimensionMinMax } from './BrushController'; import ExtensionAPI from '../../core/ExtensionAPI'; import GridModel from '../../coord/cartesian/GridModel'; import GeoModel from '../../coord/geo/GeoModel'; import { CoordinateSystemMaster } from '../../coord/CoordinateSystem'; import Cartesian2D from '../../coord/cartesian/Cartesian2D'; import Geo from '../../coord/geo/Geo'; import GlobalModel from '../../model/Global'; import { BrushAreaParam, BrushAreaParamInternal } from '../brush/BrushModel'; import SeriesModel from '../../model/Series'; import { Dictionary } from '../../util/types'; import { ModelFinderObject, ModelFinder, parseFinder as modelUtilParseFinder, ParsedModelFinderKnown } from '../../util/model'; type COORD_CONVERTS_INDEX = 0 | 1; // FIXME // how to genarialize to more coordinate systems. const INCLUDE_FINDER_MAIN_TYPES = [ 'grid', 'xAxis', 'yAxis', 'geo', 'graph', 'polar', 'radiusAxis', 'angleAxis', 'bmap' ]; type BrushableCoordinateSystem = Cartesian2D | Geo; type BrushTargetBuilderKey = 'grid' | 'geo'; /** * There can be multiple axes in a single targetInfo. Consider the case * of `grid` component, a targetInfo represents a grid which contains one or more * cartesian and one or more axes. And consider the case of parallel system, * which has multiple axes in a coordinate system. */ interface BrushTargetInfo { panelId: string; coordSysModel: CoordinateSystemMaster['model']; // Use the first one as the representitive coordSys. // A representitive cartesian in grid (first cartesian by default). coordSys: BrushableCoordinateSystem; // All cartesians. coordSyses: BrushableCoordinateSystem[]; getPanelRect: GetPanelRect, } export interface BrushTargetInfoCartesian2D extends BrushTargetInfo { gridModel: GridModel; coordSys: Cartesian2D; coordSyses: Cartesian2D[]; xAxisDeclared: boolean; yAxisDeclared: boolean; } export interface BrushTargetInfoGeo extends BrushTargetInfo { geoModel: GeoModel, coordSysModel: GeoModel, coordSys: Geo, coordSyses: Geo[], } type GetPanelRect = () => graphic.BoundingRect; class BrushTargetManager { private _targetInfoList: BrushTargetInfo[] = []; /** * @param finder contains Index/Id/Name of xAxis/yAxis/geo/grid * Each can be {number|Array.}. like: {xAxisIndex: [3, 4]} * @param opt.include include coordinate system types. */ constructor( finder: ModelFinderObject, ecModel: GlobalModel, opt?: {include?: BrushTargetBuilderKey[]} ) { const foundCpts = parseFinder(ecModel, finder); each(targetInfoBuilders, (builder, type) => { if (!opt || !opt.include || indexOf(opt.include, type) >= 0) { builder(foundCpts, this._targetInfoList); } }); } setOutputRanges( areas: BrushControllerEvents['brush']['areas'], ecModel: GlobalModel ): BrushAreaParam[] { this.matchOutputRanges(areas, ecModel, function ( area: BrushAreaParam, coordRange: ReturnType['values'], coordSys: BrushableCoordinateSystem ) { (area.coordRanges || (area.coordRanges = [])).push(coordRange); // area.coordRange is the first of area.coordRanges if (!area.coordRange) { area.coordRange = coordRange; // In 'category' axis, coord to pixel is not reversible, so we can not // rebuild range by coordRange accrately, which may bring trouble when // brushing only one item. So we use __rangeOffset to rebuilding range // by coordRange. And this it only used in brush component so it is no // need to be adapted to coordRanges. const result = coordConvert[area.brushType](0, coordSys, coordRange); area.__rangeOffset = { offset: diffProcessor[area.brushType](result.values, area.range, [1, 1]), xyMinMax: result.xyMinMax }; } }); return areas; } matchOutputRanges[0] & { brushType: BrushType; range: BrushAreaRange; } )>( areas: T[], ecModel: GlobalModel, cb: ( area: T, coordRange: ReturnType['values'], coordSys: BrushableCoordinateSystem, ecModel: GlobalModel ) => void ) { each(areas, function (area) { const targetInfo = this.findTargetInfo(area, ecModel); if (targetInfo && targetInfo !== true) { each( targetInfo.coordSyses, function (coordSys) { const result = coordConvert[area.brushType](1, coordSys, area.range, true); cb(area, result.values, coordSys, ecModel); } ); } }, this); } /** * the `areas` is `BrushModel.areas`. * Called in layout stage. * convert `area.coordRange` to global range and set panelId to `area.range`. */ setInputRanges( areas: BrushAreaParamInternal[], ecModel: GlobalModel ): void { each(areas, function (area) { const targetInfo = this.findTargetInfo(area, ecModel); if (__DEV__) { assert( !targetInfo || targetInfo === true || area.coordRange, 'coordRange must be specified when coord index specified.' ); assert( !targetInfo || targetInfo !== true || area.range, 'range must be specified in global brush.' ); } area.range = area.range || []; // convert coordRange to global range and set panelId. if (targetInfo && targetInfo !== true) { area.panelId = targetInfo.panelId; // (1) area.range should always be calculate from coordRange but does // not keep its original value, for the sake of the dataZoom scenario, // where area.coordRange remains unchanged but area.range may be changed. // (2) Only support converting one coordRange to pixel range in brush // component. So do not consider `coordRanges`. // (3) About __rangeOffset, see comment above. const result = coordConvert[area.brushType](0, targetInfo.coordSys, area.coordRange); const rangeOffset = area.__rangeOffset; area.range = rangeOffset ? diffProcessor[area.brushType]( result.values, rangeOffset.offset, getScales(result.xyMinMax, rangeOffset.xyMinMax) ) : result.values; } }, this); } makePanelOpts( api: ExtensionAPI, getDefaultBrushType?: (targetInfo: BrushTargetInfo) => BrushType ): BrushPanelConfig[] { return map(this._targetInfoList, function (targetInfo) { const rect = targetInfo.getPanelRect(); return { panelId: targetInfo.panelId, defaultBrushType: getDefaultBrushType ? getDefaultBrushType(targetInfo) : null, clipPath: brushHelper.makeRectPanelClipPath(rect), isTargetByCursor: brushHelper.makeRectIsTargetByCursor( rect, api, targetInfo.coordSysModel ), getLinearBrushOtherExtent: brushHelper.makeLinearBrushOtherExtent(rect) }; }); } controlSeries(area: BrushAreaParamInternal, seriesModel: SeriesModel, ecModel: GlobalModel): boolean { // Check whether area is bound in coord, and series do not belong to that coord. // If do not do this check, some brush (like lineX) will controll all axes. const targetInfo = this.findTargetInfo(area, ecModel); return targetInfo === true || ( targetInfo && indexOf( targetInfo.coordSyses, seriesModel.coordinateSystem as BrushableCoordinateSystem ) >= 0 ); } /** * If return Object, a coord found. * If return true, global found. * Otherwise nothing found. */ findTargetInfo( area: ModelFinderObject & { panelId?: string }, ecModel: GlobalModel ): BrushTargetInfo | true { const targetInfoList = this._targetInfoList; const foundCpts = parseFinder(ecModel, area); for (let i = 0; i < targetInfoList.length; i++) { const targetInfo = targetInfoList[i]; const areaPanelId = area.panelId; if (areaPanelId) { if (targetInfo.panelId === areaPanelId) { return targetInfo; } } else { for (let j = 0; j < targetInfoMatchers.length; j++) { if (targetInfoMatchers[j](foundCpts, targetInfo)) { return targetInfo; } } } } return true; } } function formatMinMax(minMax: BrushDimensionMinMax): BrushDimensionMinMax { minMax[0] > minMax[1] && minMax.reverse(); return minMax; } function parseFinder( ecModel: GlobalModel, finder: ModelFinder ): ParsedModelFinderKnown { return modelUtilParseFinder( ecModel, finder, {includeMainTypes: INCLUDE_FINDER_MAIN_TYPES} ); } type TargetInfoBuilder = ( foundCpts: ParsedModelFinderKnown, targetInfoList: BrushTargetInfo[] ) => void; const targetInfoBuilders: Record = { grid: function (foundCpts, targetInfoList) { const xAxisModels = foundCpts.xAxisModels; const yAxisModels = foundCpts.yAxisModels; const gridModels = foundCpts.gridModels; // Remove duplicated. const gridModelMap = createHashMap(); const xAxesHas = {} as Dictionary; const yAxesHas = {} as Dictionary; if (!xAxisModels && !yAxisModels && !gridModels) { return; } each(xAxisModels, function (axisModel) { const gridModel = axisModel.axis.grid.model; gridModelMap.set(gridModel.id, gridModel); xAxesHas[gridModel.id] = true; }); each(yAxisModels, function (axisModel) { const gridModel = axisModel.axis.grid.model; gridModelMap.set(gridModel.id, gridModel); yAxesHas[gridModel.id] = true; }); each(gridModels, function (gridModel) { gridModelMap.set(gridModel.id, gridModel); xAxesHas[gridModel.id] = true; yAxesHas[gridModel.id] = true; }); gridModelMap.each(function (gridModel) { const grid = gridModel.coordinateSystem; const cartesians = [] as Cartesian2D[]; each(grid.getCartesians(), function (cartesian, index) { if (indexOf(xAxisModels, cartesian.getAxis('x').model) >= 0 || indexOf(yAxisModels, cartesian.getAxis('y').model) >= 0 ) { cartesians.push(cartesian); } }); targetInfoList.push({ panelId: 'grid--' + gridModel.id, gridModel: gridModel, coordSysModel: gridModel, // Use the first one as the representitive coordSys. coordSys: cartesians[0], coordSyses: cartesians, getPanelRect: panelRectBuilders.grid, xAxisDeclared: xAxesHas[gridModel.id], yAxisDeclared: yAxesHas[gridModel.id] } as BrushTargetInfoCartesian2D); }); }, geo: function (foundCpts, targetInfoList) { each(foundCpts.geoModels, function (geoModel: GeoModel) { const coordSys = geoModel.coordinateSystem; targetInfoList.push({ panelId: 'geo--' + geoModel.id, geoModel: geoModel, coordSysModel: geoModel, coordSys: coordSys, coordSyses: [coordSys], getPanelRect: panelRectBuilders.geo } as BrushTargetInfoGeo); }); } }; type TargetInfoMatcher = ( foundCpts: ParsedModelFinderKnown, targetInfo: BrushTargetInfo ) => boolean; const targetInfoMatchers: TargetInfoMatcher[] = [ // grid function (foundCpts, targetInfo) { const xAxisModel = foundCpts.xAxisModel; const yAxisModel = foundCpts.yAxisModel; let gridModel = foundCpts.gridModel; !gridModel && xAxisModel && (gridModel = xAxisModel.axis.grid.model); !gridModel && yAxisModel && (gridModel = yAxisModel.axis.grid.model); return gridModel && gridModel === (targetInfo as BrushTargetInfoCartesian2D).gridModel; }, // geo function (foundCpts, targetInfo) { const geoModel = foundCpts.geoModel; return geoModel && geoModel === (targetInfo as BrushTargetInfoGeo).geoModel; } ]; type PanelRectBuilder = (this: BrushTargetInfo) => graphic.BoundingRect; const panelRectBuilders: Record = { grid: function (this: BrushTargetInfoCartesian2D) { // grid is not Transformable. return this.coordSys.master.getRect().clone(); }, geo: function (this: BrushTargetInfoGeo) { const coordSys = this.coordSys; const rect = coordSys.getBoundingRect().clone(); // geo roam and zoom transform rect.applyTransform(graphic.getTransform(coordSys)); return rect; } }; type ConvertCoord = ( to: COORD_CONVERTS_INDEX, coordSys: BrushableCoordinateSystem, rangeOrCoordRange: BrushAreaRange, clamp?: boolean ) => { values: BrushAreaRange, xyMinMax: BrushDimensionMinMax[] }; const coordConvert: Record = { lineX: curry(axisConvert, 0), lineY: curry(axisConvert, 1), rect: function (to, coordSys, rangeOrCoordRange: BrushDimensionMinMax[], clamp): { values: BrushDimensionMinMax[], xyMinMax: BrushDimensionMinMax[] } { const xminymin = to ? coordSys.pointToData([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]], clamp) : coordSys.dataToPoint([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]], clamp); const xmaxymax = to ? coordSys.pointToData([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]], clamp) : coordSys.dataToPoint([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]], clamp); const values = [ formatMinMax([xminymin[0], xmaxymax[0]]), formatMinMax([xminymin[1], xmaxymax[1]]) ]; return {values: values, xyMinMax: values}; }, polygon: function (to, coordSys, rangeOrCoordRange: BrushDimensionMinMax[], clamp): { values: BrushDimensionMinMax[], xyMinMax: BrushDimensionMinMax[] } { const xyMinMax = [[Infinity, -Infinity], [Infinity, -Infinity]]; const values = map(rangeOrCoordRange, function (item) { const p = to ? coordSys.pointToData(item, clamp) : coordSys.dataToPoint(item, clamp); xyMinMax[0][0] = Math.min(xyMinMax[0][0], p[0]); xyMinMax[1][0] = Math.min(xyMinMax[1][0], p[1]); xyMinMax[0][1] = Math.max(xyMinMax[0][1], p[0]); xyMinMax[1][1] = Math.max(xyMinMax[1][1], p[1]); return p; }); return {values: values, xyMinMax: xyMinMax}; } }; function axisConvert( axisNameIndex: 0 | 1, to: COORD_CONVERTS_INDEX, coordSys: Cartesian2D, rangeOrCoordRange: BrushDimensionMinMax ): { values: BrushDimensionMinMax, xyMinMax: BrushDimensionMinMax[] } { if (__DEV__) { assert( coordSys.type === 'cartesian2d', 'lineX/lineY brush is available only in cartesian2d.' ); } const axis = coordSys.getAxis(['x', 'y'][axisNameIndex]); const values = formatMinMax(map([0, 1], function (i) { return to ? axis.coordToData(axis.toLocalCoord(rangeOrCoordRange[i]), true) : axis.toGlobalCoord(axis.dataToCoord(rangeOrCoordRange[i])); })); const xyMinMax = []; xyMinMax[axisNameIndex] = values; xyMinMax[1 - axisNameIndex] = [NaN, NaN]; return {values: values, xyMinMax: xyMinMax}; } type DiffProcess = ( values: BrushDimensionMinMax | BrushDimensionMinMax[], refer: BrushDimensionMinMax | BrushDimensionMinMax[], scales: ReturnType ) => BrushDimensionMinMax | BrushDimensionMinMax[]; const diffProcessor: Record = { lineX: curry(axisDiffProcessor, 0), lineY: curry(axisDiffProcessor, 1), rect: function ( values: BrushDimensionMinMax[], refer: BrushDimensionMinMax[], scales: ReturnType ): BrushDimensionMinMax[] { return [ [values[0][0] - scales[0] * refer[0][0], values[0][1] - scales[0] * refer[0][1]], [values[1][0] - scales[1] * refer[1][0], values[1][1] - scales[1] * refer[1][1]] ]; }, polygon: function ( values: BrushDimensionMinMax[], refer: BrushDimensionMinMax[], scales: ReturnType ): BrushDimensionMinMax[] { return map(values, function (item, idx) { return [item[0] - scales[0] * refer[idx][0], item[1] - scales[1] * refer[idx][1]]; }); } }; function axisDiffProcessor( axisNameIndex: 0 | 1, values: BrushDimensionMinMax, refer: BrushDimensionMinMax, scales: ReturnType ): BrushDimensionMinMax { return [ values[0] - scales[axisNameIndex] * refer[0], values[1] - scales[axisNameIndex] * refer[1] ]; } // We have to process scale caused by dataZoom manually, // although it might be not accurate. // Return [0~1, 0~1] function getScales(xyMinMaxCurr: BrushDimensionMinMax[], xyMinMaxOrigin: BrushDimensionMinMax[]): number[] { const sizeCurr = getSize(xyMinMaxCurr); const sizeOrigin = getSize(xyMinMaxOrigin); const scales = [sizeCurr[0] / sizeOrigin[0], sizeCurr[1] / sizeOrigin[1]]; isNaN(scales[0]) && (scales[0] = 1); isNaN(scales[1]) && (scales[1] = 1); return scales; } function getSize(xyMinMax: BrushDimensionMinMax[]): number[] { return xyMinMax ? [xyMinMax[0][1] - xyMinMax[0][0], xyMinMax[1][1] - xyMinMax[1][0]] : [NaN, NaN]; } export default BrushTargetManager;