diff --git a/lerna.json b/lerna.json index af812533c1..298f89b45d 100644 --- a/lerna.json +++ b/lerna.json @@ -14,7 +14,7 @@ "message": "chore: publish" } }, - "version": "2.0.22", + "version": "2.0.23-alpha.0", "npmClient": "yarn", "useWorkspaces": true, "publishConfig": { diff --git a/package.json b/package.json index 42f687a91f..a14beaa9ec 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "stylelint-config-styled-components": "^0.1.1", "stylelint-processor-styled-components": "^1.3.2", "svg-inline-loader": "^0.8.0", + "tapable": "^1.1.3", "ts-jest": "^24.0.2", "tslint": "^5.11.0", "tslint-config-prettier": "^1.15.0", diff --git a/packages/component/package.json b/packages/component/package.json index 0a2375926f..63f74b6536 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-component", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -24,8 +24,8 @@ "author": "lzxue", "license": "ISC", "dependencies": { - "@antv/l7-core": "^2.0.22", - "@antv/l7-utils": "^2.0.22", + "@antv/l7-core": "^2.0.23-alpha.0", + "@antv/l7-utils": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "eventemitter3": "^4.0.0", "inversify": "^5.0.1", diff --git a/packages/core/package.json b/packages/core/package.json index b252155799..2e5ff0ee42 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-core", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -22,7 +22,7 @@ "author": "xiaoiver", "license": "ISC", "dependencies": { - "@antv/l7-utils": "^2.0.22", + "@antv/l7-utils": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "@mapbox/tiny-sdf": "^1.1.1", "ajv": "^6.10.2", diff --git a/packages/core/src/inversify.config.ts b/packages/core/src/inversify.config.ts index 6e97dea9f4..93a158f0cc 100644 --- a/packages/core/src/inversify.config.ts +++ b/packages/core/src/inversify.config.ts @@ -17,6 +17,7 @@ import { IControlService } from './services/component/IControlService'; import { IGlobalConfigService } from './services/config/IConfigService'; import { ICoordinateSystemService } from './services/coordinate/ICoordinateSystemService'; import { IInteractionService } from './services/interaction/IInteractionService'; +import { IPickingService } from './services/interaction/IPickingService'; import { ILayerService } from './services/layer/ILayerService'; import { IStyleAttributeService } from './services/layer/IStyleAttributeService'; import { ILogService } from './services/log/ILogService'; @@ -33,6 +34,7 @@ import PopupService from './services/component/PopupService'; import GlobalConfigService from './services/config/ConfigService'; import CoordinateSystemService from './services/coordinate/CoordinateSystemService'; import InteractionService from './services/interaction/InteractionService'; +import PickingService from './services/interaction/PickingService'; import LayerService from './services/layer/LayerService'; import StyleAttributeService from './services/layer/StyleAttributeService'; import LogService from './services/log/LogService'; @@ -180,6 +182,10 @@ export function createSceneContainer() { .bind(TYPES.IInteractionService) .to(InteractionService) .inSingletonScope(); + sceneContainer + .bind(TYPES.IPickingService) + .to(PickingService) + .inSingletonScope(); sceneContainer .bind(TYPES.IControlService) .to(ControlService) diff --git a/packages/core/src/services/config/ConfigService.ts b/packages/core/src/services/config/ConfigService.ts index cdbffd5c0e..9255466a11 100644 --- a/packages/core/src/services/config/ConfigService.ts +++ b/packages/core/src/services/config/ConfigService.ts @@ -55,7 +55,7 @@ const defaultLayerConfig: Partial = { zIndex: 0, blend: 'normal', pickedFeatureID: -1, - enableMultiPassRenderer: true, + enableMultiPassRenderer: false, enablePicking: true, active: false, activeColor: '#2f54eb', diff --git a/packages/core/src/services/interaction/IPickingService.ts b/packages/core/src/services/interaction/IPickingService.ts new file mode 100644 index 0000000000..c118cfc4ef --- /dev/null +++ b/packages/core/src/services/interaction/IPickingService.ts @@ -0,0 +1,3 @@ +export interface IPickingService { + init(): void; +} diff --git a/packages/core/src/services/interaction/PickingService.ts b/packages/core/src/services/interaction/PickingService.ts new file mode 100644 index 0000000000..cc27a7eb6d --- /dev/null +++ b/packages/core/src/services/interaction/PickingService.ts @@ -0,0 +1,275 @@ +import { decodePickingColor, encodePickingColor } from '@antv/l7-utils'; +import { inject, injectable } from 'inversify'; +import { + IMapService, + IRendererService, + IShaderModuleService, +} from '../../index'; +import { TYPES } from '../../types'; +import { ICameraService } from '../camera/ICameraService'; +import { + IInteractionService, + IInteractionTarget, + InteractionEvent, +} from '../interaction/IInteractionService'; +import { ILayer, ILayerService } from '../layer/ILayerService'; +import { ILngLat } from '../map/IMapService'; +import { gl } from '../renderer/gl'; +import { IFramebuffer } from '../renderer/IFramebuffer'; +import { IPickingService } from './IPickingService'; + +const PICKSCALE = 10.0; +@injectable() +export default class PickingService implements IPickingService { + @inject(TYPES.IRendererService) + private rendererService: IRendererService; + + @inject(TYPES.IInteractionService) + private interactionService: IInteractionService; + + @inject(TYPES.ILayerService) + private layerService: ILayerService; + private pickingFBO: IFramebuffer; + + private width: number = 0; + + private height: number = 0; + + private alreadyInPicking: boolean = false; + + public init() { + const { + createTexture2D, + createFramebuffer, + getViewportSize, + } = this.rendererService; + const { width, height } = getViewportSize(); + // 创建 picking framebuffer,后续实时 resize + this.pickingFBO = createFramebuffer({ + color: createTexture2D({ + width, + height, + wrapS: gl.CLAMP_TO_EDGE, + wrapT: gl.CLAMP_TO_EDGE, + }), + }); + + // 监听 hover 事件 + this.interactionService.on( + InteractionEvent.Hover, + this.pickingAllLayer.bind(this), + ); + } + private async pickingAllLayer(target: IInteractionTarget) { + if (this.alreadyInPicking || this.layerService.alreadyInRendering) { + return; + } + this.alreadyInPicking = true; + await this.pickingLayers(target); + this.layerService.renderLayers(); + this.alreadyInPicking = false; + } + private async pickingLayers(target: IInteractionTarget) { + const { getViewportSize, useFramebuffer, clear } = this.rendererService; + const { width, height } = getViewportSize(); + + if (this.width !== width || this.height !== height) { + this.pickingFBO.resize({ + width: Math.round(width / PICKSCALE), + height: Math.round(height / PICKSCALE), + }); + this.width = width; + this.height = height; + } + + useFramebuffer(this.pickingFBO, () => { + const layers = this.layerService.getLayers(); + layers + .filter((layer) => layer.needPick()) + .reverse() + .forEach((layer) => { + clear({ + framebuffer: this.pickingFBO, + color: [0, 0, 0, 0], + stencil: 0, + depth: 1, + }); + + layer.hooks.beforePickingEncode.call(); + layer.renderModels(); + layer.hooks.afterPickingEncode.call(); + this.pickFromPickingFBO(layer, target); + }); + }); + } + private async pickingLayer(layer: ILayer, target: IInteractionTarget) { + const { getViewportSize, useFramebuffer, clear } = this.rendererService; + const { width, height } = getViewportSize(); + + if (this.width !== width || this.height !== height) { + this.pickingFBO.resize({ + width: Math.round(width / PICKSCALE), + height: Math.round(height / PICKSCALE), + }); + this.width = width; + this.height = height; + } + + useFramebuffer(this.pickingFBO, () => { + clear({ + framebuffer: this.pickingFBO, + color: [0, 0, 0, 0], + stencil: 0, + depth: 1, + }); + + layer.hooks.beforePickingEncode.call(); + layer.renderModels(); + layer.hooks.afterPickingEncode.call(); + this.pickFromPickingFBO(layer, target); + }); + } + + private pickFromPickingFBO = ( + layer: ILayer, + { x, y, lngLat, type }: IInteractionTarget, + ) => { + const { + getViewportSize, + readPixels, + useFramebuffer, + } = this.rendererService; + const { width, height } = getViewportSize(); + + const { enableHighlight, enableSelect } = layer.getLayerConfig(); + + const xInDevicePixel = x * window.devicePixelRatio; + const yInDevicePixel = y * window.devicePixelRatio; + if ( + xInDevicePixel > width || + xInDevicePixel < 0 || + yInDevicePixel > height || + yInDevicePixel < 0 + ) { + return; + } + let pickedColors: Uint8Array | undefined; + pickedColors = readPixels({ + x: Math.round(xInDevicePixel / PICKSCALE), + // 视口坐标系原点在左上,而 WebGL 在左下,需要翻转 Y 轴 + y: Math.round((height - (y + 1) * window.devicePixelRatio) / PICKSCALE), + width: 1, + height: 1, + data: new Uint8Array(1 * 1 * 4), + framebuffer: this.pickingFBO, + }); + if ( + pickedColors[0] !== 0 || + pickedColors[1] !== 0 || + pickedColors[2] !== 0 + ) { + const pickedFeatureIdx = decodePickingColor(pickedColors); + const rawFeature = layer.getSource().getFeatureById(pickedFeatureIdx); + const target = { + x, + y, + type, + lngLat, + featureId: pickedFeatureIdx, + feature: rawFeature, + }; + if (!rawFeature) { + // this.logger.error( + // '未找到颜色编码解码后的原始 feature,请检查 fragment shader 中末尾是否添加了 `gl_FragColor = filterColor(gl_FragColor);`', + // ); + } else { + // trigger onHover/Click callback on layer + layer.setCurrentPickId(pickedFeatureIdx); + this.triggerHoverOnLayer(layer, target); + } + } else { + const target = { + x, + y, + lngLat, + type: layer.getCurrentPickId() === null ? 'un' + type : 'mouseout', + featureId: null, + feature: null, + }; + this.triggerHoverOnLayer(layer, { + ...target, + type: 'unpick', + }); + this.triggerHoverOnLayer(layer, target); + layer.setCurrentPickId(null); + } + + if (enableHighlight) { + this.highlightPickedFeature(layer, pickedColors); + } + if ( + enableSelect && + type === 'click' && + pickedColors?.toString() !== [0, 0, 0, 0].toString() + ) { + this.selectFeature(layer, pickedColors); + } + }; + private triggerHoverOnLayer( + layer: ILayer, + target: { + x: number; + y: number; + type: string; + lngLat: ILngLat; + feature: unknown; + featureId: number | null; + }, + ) { + layer.emit(target.type, target); + } + + /** + * highlight 如果直接修改选中 feature 的 buffer,存在两个问题: + * 1. 鼠标移走时无法恢复 + * 2. 无法实现高亮颜色与原始原色的 alpha 混合 + * 因此高亮还是放在 shader 中做比较好 + * @example + * this.layer.color('name', ['#000000'], { + * featureRange: { + * startIndex: pickedFeatureIdx, + * endIndex: pickedFeatureIdx + 1, + * }, + * }); + */ + private highlightPickedFeature( + layer: ILayer, + pickedColors: Uint8Array | undefined, + ) { + const [r, g, b] = pickedColors; + layer.hooks.beforeHighlight.call([r, g, b]); + // this.layerService.renderLayers(); + } + + private selectFeature(layer: ILayer, pickedColors: Uint8Array | undefined) { + const [r, g, b] = pickedColors; + layer.hooks.beforeSelect.call([r, g, b]); + // this.layerService.renderLayers(); + } + + private selectFeatureHandle( + layer: ILayer, + { featureId }: Partial, + ) { + const pickedColors = encodePickingColor(featureId as number); + this.selectFeature(layer, new Uint8Array(pickedColors)); + } + + private highlightFeatureHandle( + layer: ILayer, + { featureId }: Partial, + ) { + const pickedColors = encodePickingColor(featureId as number); + this.highlightPickedFeature(layer, new Uint8Array(pickedColors)); + } +} diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index 7638e2458c..9f7fb07d14 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -22,6 +22,7 @@ import { IScale, IScaleOptions, IStyleAttributeService, + ScaleAttributeType, StyleAttrField, StyleAttributeOption, Triangulation, @@ -111,13 +112,14 @@ export interface ILayer { setCurrentPickId(id: number | null): void; getCurrentPickId(): number | null; prepareBuildModel(): void; + renderModels(): void; buildModels(): void; buildLayerModel( options: ILayerModelInitializationOptions & Partial, ): IModel; init(): ILayer; - scale(field: string | IScaleOptions, cfg?: IScale): ILayer; + scale(field: string | number | IScaleOptions, cfg?: IScale): ILayer; size(field: StyleAttrField, value?: StyleAttributeOption): ILayer; color(field: StyleAttrField, value?: StyleAttributeOption): ILayer; shape(field: StyleAttrField, value?: StyleAttributeOption): ILayer; @@ -138,6 +140,7 @@ export interface ILayer { style(options: unknown): ILayer; hide(): ILayer; show(): ILayer; + getLegendItems(name: string): any; setIndex(index: number): ILayer; isVisible(): boolean; setMaxZoom(min: number): ILayer; @@ -263,6 +266,7 @@ export interface ILayerConfig { */ export interface ILayerService { clock: Clock; + alreadyInRendering: boolean; add(layer: ILayer): void; initLayers(): void; startAnimate(): void; diff --git a/packages/core/src/services/layer/IStyleAttributeService.ts b/packages/core/src/services/layer/IStyleAttributeService.ts index d8a65e6f9c..bf81e70cf4 100644 --- a/packages/core/src/services/layer/IStyleAttributeService.ts +++ b/packages/core/src/services/layer/IStyleAttributeService.ts @@ -35,8 +35,11 @@ export type ScaleTypeName = | 'quantize' | 'threshold' | 'cat'; + +export type ScaleAttributeType = 'color' | 'size' | 'shape'; export interface IScale { type: ScaleTypeName; + field?: string; ticks?: any[]; nice?: boolean; format?: () => any; @@ -49,6 +52,7 @@ export enum StyleScaleType { } export interface IScaleOption { field?: string; + attr?: ScaleAttributeType; type: ScaleTypeName; ticks?: any[]; nice?: boolean; @@ -115,6 +119,11 @@ type CallBack = (...args: any[]) => any; export type StyleAttributeField = string | string[] | number[]; export type StyleAttributeOption = string | number | boolean | any[] | CallBack; export type StyleAttrField = string | string[] | number | number[]; +export interface IAttributeScale { + field: string | number; + func: unknown; + option: IScaleOption | undefined; +} export interface IStyleAttributeInitializationOptions { name: string; @@ -125,10 +134,7 @@ export interface IStyleAttributeInitializationOptions { names: string[] | number[]; type: StyleScaleType; callback?: (...args: any[]) => []; - scalers?: Array<{ - field: string | number; - func: unknown; - }>; + scalers?: IAttributeScale[]; }; descriptor: IVertexAttributeDescriptor; } @@ -186,6 +192,7 @@ export interface IStyleAttributeService { ): void; getLayerStyleAttributes(): IStyleAttribute[] | undefined; getLayerStyleAttribute(attributeName: string): IStyleAttribute | undefined; + getLayerAttributeScale(attributeName: string): any; createAttributesAndIndices( encodedFeatures: IEncodeFeature[], triangulation?: Triangulation, diff --git a/packages/core/src/services/layer/LayerService.ts b/packages/core/src/services/layer/LayerService.ts index 1f2c46e9e3..746676bd50 100644 --- a/packages/core/src/services/layer/LayerService.ts +++ b/packages/core/src/services/layer/LayerService.ts @@ -10,6 +10,8 @@ import { ILayerModel, ILayerService } from './ILayerService'; export default class LayerService implements ILayerService { public clock = new Clock(); + public alreadyInRendering: boolean = false; + private layers: ILayer[] = []; private layerRenderID: number; @@ -18,8 +20,6 @@ export default class LayerService implements ILayerService { private animateInstanceCount: number = 0; - private alreadyInRendering: boolean = false; - @inject(TYPES.IRendererService) private readonly renderService: IRendererService; diff --git a/packages/core/src/services/layer/StyleAttribute.ts b/packages/core/src/services/layer/StyleAttribute.ts index 0e61044104..e107499ba1 100644 --- a/packages/core/src/services/layer/StyleAttribute.ts +++ b/packages/core/src/services/layer/StyleAttribute.ts @@ -1,5 +1,7 @@ import { isNil } from 'lodash'; import { + IAttributeScale, + IScaleOption, IStyleAttribute, StyleScaleType, } from '../layer/IStyleAttributeService'; @@ -22,10 +24,7 @@ export default class StyleAttribute implements IStyleAttribute { field: string | string[]; values: unknown[]; callback?: (...args: any[]) => []; - scalers?: Array<{ - field: string; - func: unknown; - }>; + scalers?: IAttributeScale[]; }; public descriptor: IVertexAttributeDescriptor; public featureBufferLayout: Array<{ diff --git a/packages/core/src/services/layer/StyleAttributeService.ts b/packages/core/src/services/layer/StyleAttributeService.ts index 78494303c6..d9e690b9fc 100644 --- a/packages/core/src/services/layer/StyleAttributeService.ts +++ b/packages/core/src/services/layer/StyleAttributeService.ts @@ -7,6 +7,7 @@ import { IRendererService } from '../renderer/IRendererService'; import { IParseDataItem } from '../source/ISourceService'; import { ILayer } from './ILayerService'; import { + IAttributeScale, IEncodeFeature, IStyleAttribute, IStyleAttributeInitializationOptions, @@ -108,6 +109,15 @@ export default class StyleAttributeService implements IStyleAttributeService { ); } + public getLayerAttributeScale(name: string) { + const attribute = this.getLayerStyleAttribute(name); + const scale = attribute?.scale?.scalers as IAttributeScale[]; + if (scale && scale[0]) { + return scale[0].func; + } + return null; + } + public updateAttributeByFeatureRange( attributeName: string, features: IEncodeFeature[], diff --git a/packages/core/src/services/renderer/passes/MultiPassRenderer.ts b/packages/core/src/services/renderer/passes/MultiPassRenderer.ts index 3a6f1e4bf4..43e75ad7af 100644 --- a/packages/core/src/services/renderer/passes/MultiPassRenderer.ts +++ b/packages/core/src/services/renderer/passes/MultiPassRenderer.ts @@ -38,6 +38,10 @@ export default class MultiPassRenderer implements IMultiPassRenderer { private layer: ILayer; private renderFlag: boolean; + private width: number = 0; + + private height: number = 0; + public setLayer(layer: ILayer) { this.layer = layer; } @@ -58,11 +62,16 @@ export default class MultiPassRenderer implements IMultiPassRenderer { for (const pass of this.passes) { await pass.render(this.layer); } - await this.postProcessor.render(this.layer); + this.layer.renderModels(); + // await this.postProcessor.render(this.layer); } public resize(width: number, height: number) { - this.postProcessor.resize(width, height); + if (this.width !== width || this.height !== height) { + this.postProcessor.resize(width, height); + this.width = width; + this.height = height; + } } public add(pass: IPass, config?: Partial) { diff --git a/packages/core/src/services/renderer/passes/PixelPickingPass.ts b/packages/core/src/services/renderer/passes/PixelPickingPass.ts index 6f678412cb..bd93335ae4 100644 --- a/packages/core/src/services/renderer/passes/PixelPickingPass.ts +++ b/packages/core/src/services/renderer/passes/PixelPickingPass.ts @@ -60,7 +60,6 @@ export default class PixelPickingPass< getViewportSize, } = this.rendererService; const { width, height } = getViewportSize(); - // 创建 picking framebuffer,后续实时 resize this.pickingFBO = createFramebuffer({ color: createTexture2D({ diff --git a/packages/core/src/services/scene/ISceneService.ts b/packages/core/src/services/scene/ISceneService.ts index 03748bc587..88d247044a 100644 --- a/packages/core/src/services/scene/ISceneService.ts +++ b/packages/core/src/services/scene/ISceneService.ts @@ -14,4 +14,9 @@ export interface ISceneService { destroy(): void; } // scene 事件 -export const SceneEventList = ['loaded', 'maploaded', 'resize', 'destroy']; +export const SceneEventList: string[] = [ + 'loaded', + 'maploaded', + 'resize', + 'destroy', +]; diff --git a/packages/core/src/services/scene/SceneService.ts b/packages/core/src/services/scene/SceneService.ts index e60adb1692..d026c724c2 100644 --- a/packages/core/src/services/scene/SceneService.ts +++ b/packages/core/src/services/scene/SceneService.ts @@ -14,6 +14,7 @@ import { IPopupService } from '../component/IPopupService'; import { IGlobalConfigService, ISceneConfig } from '../config/IConfigService'; import { ICoordinateSystemService } from '../coordinate/ICoordinateSystemService'; import { IInteractionService } from '../interaction/IInteractionService'; +import { IPickingService } from '../interaction/IPickingService'; import { ILayer, ILayerService } from '../layer/ILayerService'; import { ILogService } from '../log/ILogService'; import { IMapCamera, IMapService } from '../map/IMapService'; @@ -64,6 +65,9 @@ export default class Scene extends EventEmitter implements ISceneService { @inject(TYPES.IInteractionService) private readonly interactionService: IInteractionService; + @inject(TYPES.IPickingService) + private readonly pickingService: IPickingService; + @inject(TYPES.IShaderModuleService) private readonly shaderModuleService: IShaderModuleService; @@ -150,6 +154,7 @@ export default class Scene extends EventEmitter implements ISceneService { this.popupService.initPopup(); // 地图初始化之后 才能初始化 container 上的交互 this.interactionService.init(); + this.logger.debug(`map ${this.id} loaded`); }); @@ -174,6 +179,7 @@ export default class Scene extends EventEmitter implements ISceneService { } else { this.logger.error('容器 id 不存在'); } + this.pickingService.init(); this.logger.debug(`scene ${this.id} renderer loaded`); }); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c2570b616f..e915b9b446 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -17,6 +17,7 @@ const TYPES = { IIconService: Symbol.for('IIconService'), IFontService: Symbol.for('IFontService'), IInteractionService: Symbol.for('IInteractionService'), + IPickingService: Symbol.for('IPickingService'), IControlService: Symbol.for('IControlService'), IStyleAttributeService: Symbol.for('IStyleAttributeService'), ILayer: Symbol.for('ILayer'), diff --git a/packages/l7/package.json b/packages/l7/package.json index 444de0b8ec..22c3501de7 100644 --- a/packages/l7/package.json +++ b/packages/l7/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "A Large-scale WebGL-powered Geospatial Data Visualization", "main": "lib/index.js", "module": "es/index.js", @@ -24,11 +24,11 @@ "author": "antv", "license": "MIT", "dependencies": { - "@antv/l7-component": "^2.0.22", - "@antv/l7-core": "^2.0.22", - "@antv/l7-layers": "^2.0.22", - "@antv/l7-maps": "^2.0.22", - "@antv/l7-scene": "^2.0.22", + "@antv/l7-component": "^2.0.23-alpha.0", + "@antv/l7-core": "^2.0.23-alpha.0", + "@antv/l7-layers": "^2.0.23-alpha.0", + "@antv/l7-maps": "^2.0.23-alpha.0", + "@antv/l7-scene": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7" }, "gitHead": "f2bd3c6473df79d815467b1677c6f985cf68800e", diff --git a/packages/layers/package.json b/packages/layers/package.json index 37a7edb359..9f53da5f5d 100644 --- a/packages/layers/package.json +++ b/packages/layers/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-layers", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "L7's collection of built-in layers", "main": "lib/index.js", "module": "es/index.js", @@ -22,9 +22,9 @@ "author": "xiaoiver", "license": "ISC", "dependencies": { - "@antv/l7-core": "^2.0.22", - "@antv/l7-source": "^2.0.22", - "@antv/l7-utils": "^2.0.22", + "@antv/l7-core": "^2.0.23-alpha.0", + "@antv/l7-source": "^2.0.23-alpha.0", + "@antv/l7-utils": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "@mapbox/martini": "^0.1.0", "@turf/meta": "^6.0.2", diff --git a/packages/layers/src/core/BaseLayer.ts b/packages/layers/src/core/BaseLayer.ts index 0dae3f409d..3b4024e900 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -31,6 +31,7 @@ import { IStyleAttributeService, IStyleAttributeUpdateOptions, lazyInject, + ScaleAttributeType, ScaleTypeName, ScaleTypes, StyleAttributeField, @@ -170,6 +171,8 @@ export default class BaseLayer extends EventEmitter private aniamateStatus: boolean = false; + // private pickingPassRender: IPass<'pixelPicking'>; + constructor(config: Partial = {}) { super(); this.name = config.name || this.id; @@ -228,7 +231,10 @@ export default class BaseLayer extends EventEmitter // 设置配置项 const sceneId = this.container.get(TYPES.SceneID); // 初始化图层配置项 - this.configService.setLayerConfig(sceneId, this.id, {}); + const { enableMultiPassRenderer = false } = this.rawConfig; + this.configService.setLayerConfig(sceneId, this.id, { + enableMultiPassRenderer, + }); // 全局容器服务 @@ -297,6 +303,9 @@ export default class BaseLayer extends EventEmitter // 触发 init 生命周期插件 this.hooks.init.call(); + + // this.pickingPassRender = this.normalPassFactory('pixelPicking'); + // this.pickingPassRender.init(this); this.hooks.afterInit.call(); // 触发初始化完成事件; @@ -360,9 +369,6 @@ export default class BaseLayer extends EventEmitter updateOptions?: Partial, ) { this.updateStyleAttribute('filter', field, values, updateOptions); - // if (this.inited) { - // this.layerModelNeedUpdate = true; - // } return this; } @@ -453,7 +459,7 @@ export default class BaseLayer extends EventEmitter } return this; } - public scale(field: ScaleTypeName | IScaleOptions, cfg: IScale) { + public scale(field: string | IScaleOptions, cfg: IScale) { if (isObject(field)) { this.scaleOptions = { ...this.scaleOptions, @@ -465,11 +471,21 @@ export default class BaseLayer extends EventEmitter return this; } public render(): ILayer { - if (this.multiPassRenderer && this.multiPassRenderer.getRenderFlag()) { - this.multiPassRenderer.render(); - } else { - this.renderModels(); - } + // if ( + // this.needPick() && + // this.multiPassRenderer && + // this.multiPassRenderer.getRenderFlag() + // ) { + // this.multiPassRenderer.render(); + // } else if (this.needPick() && this.multiPassRenderer) { + // this.renderModels(); + // } else { + // this.renderModels(); + // } + + this.renderModels(); + // this.multiPassRenderer.render(); + // this.renderModels(); return this; } @@ -730,6 +746,30 @@ export default class BaseLayer extends EventEmitter } return this.configSchema; } + public getLegendItems(name: string): any { + const scale = this.styleAttributeService.getLayerAttributeScale(name); + if (scale) { + if (scale.ticks) { + const items = scale.ticks().map((item: any) => { + return { + value: item, + [name]: scale(item), + }; + }); + return items; + } else if (scale.invertExtent) { + const items = scale.range().map((item: any) => { + return { + value: scale.invertExtent(item), + [name]: item, + }; + }); + return items; + } + } else { + return []; + } + } public pick({ x, y }: { x: number; y: number }) { this.interactionService.triggerHover({ x, y }); @@ -806,11 +846,7 @@ export default class BaseLayer extends EventEmitter throw new Error('Method not implemented.'); } - protected getConfigSchema() { - throw new Error('Method not implemented.'); - } - - protected renderModels() { + public renderModels() { if (this.layerModelNeedUpdate) { this.models = this.layerModel.buildModels(); this.hooks.beforeRender.call(); @@ -824,6 +860,10 @@ export default class BaseLayer extends EventEmitter return this; } + protected getConfigSchema() { + throw new Error('Method not implemented.'); + } + protected getModelType(): unknown { throw new Error('Method not implemented.'); } diff --git a/packages/layers/src/heatmap/index.ts b/packages/layers/src/heatmap/index.ts index 86042d222d..13212b8380 100644 --- a/packages/layers/src/heatmap/index.ts +++ b/packages/layers/src/heatmap/index.ts @@ -12,19 +12,7 @@ export default class HeatMapLayer extends BaseLayer { this.layerModel = new HeatMapModels[shape](this); this.models = this.layerModel.buildModels(); } - protected getConfigSchema() { - return { - properties: { - opacity: { - type: 'number', - minimum: 0, - maximum: 1, - }, - }, - }; - } - - protected renderModels() { + public renderModels() { const shape = this.getModelType(); if (shape === 'heatmap') { // if (this.layerModelNeedUpdate) { @@ -49,6 +37,18 @@ export default class HeatMapLayer extends BaseLayer { ); return this; } + protected getConfigSchema() { + return { + properties: { + opacity: { + type: 'number', + minimum: 0, + maximum: 1, + }, + }, + }; + } + protected getModelType(): HeatMapModelType { const shapeAttribute = this.styleAttributeService.getLayerStyleAttribute( 'shape', diff --git a/packages/layers/src/plugins/FeatureScalePlugin.ts b/packages/layers/src/plugins/FeatureScalePlugin.ts index a4f136a26c..55fe809491 100644 --- a/packages/layers/src/plugins/FeatureScalePlugin.ts +++ b/packages/layers/src/plugins/FeatureScalePlugin.ts @@ -64,7 +64,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { this.caculateScalesForAttributes(attributes || [], dataArray); }); - // 检测数据是不否需要更新 + // 检测数据是否需要更新 layer.hooks.beforeRenderData.tap('FeatureScalePlugin', (flag) => { if (flag) { this.scaleOptions = layer.getScaleOptions(); @@ -146,6 +146,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { return { field: scale.field, func: scale.scale, + option: scale.option, }; }); @@ -160,13 +161,17 @@ export default class FeatureScalePlugin implements ILayerPlugin { ) { const scalekey = [field, attribute.name].join('_'); const values = attribute.scale?.values; - if (this.scaleCache[scalekey]) { - return this.scaleCache[scalekey]; - } - const styleScale = this.createScale(field, values, dataArray); - this.scaleCache[scalekey] = styleScale; - - return this.scaleCache[scalekey]; + // if (this.scaleCache[scalekey]) { + // return this.scaleCache[scalekey]; + // } + const styleScale = this.createScale( + field, + attribute.name, + values, + dataArray, + ); + // this.scaleCache[scalekey] = styleScale; + return styleScale; } /** @@ -188,11 +193,15 @@ export default class FeatureScalePlugin implements ILayerPlugin { private createScale( field: string | number, + name: string, values: unknown[] | string | undefined, data?: IParseDataItem[], ): IStyleScale { - // 首先查找全局默认配置例如 color - const scaleOption: IScale | undefined = this.scaleOptions[field]; + // scale 支持根据视觉通道和字段 + const scaleOption: IScale | undefined = + this.scaleOptions[name] && this.scaleOptions[name].field === field + ? this.scaleOptions[name] + : this.scaleOptions[field]; const styleScale: IStyleScale = { field, scale: undefined, diff --git a/packages/layers/src/plugins/MultiPassRendererPlugin.ts b/packages/layers/src/plugins/MultiPassRendererPlugin.ts index 2f8b9191e3..333bc8b712 100644 --- a/packages/layers/src/plugins/MultiPassRendererPlugin.ts +++ b/packages/layers/src/plugins/MultiPassRendererPlugin.ts @@ -76,11 +76,11 @@ export default class MultiPassRendererPlugin implements ILayerPlugin { }); layer.hooks.beforeRender.tap('MultiPassRendererPlugin', () => { - if (this.enabled) { - // 渲染前根据 viewport 调整 FBO size - const { width, height } = rendererService.getViewportSize(); - layer.multiPassRenderer.resize(width, height); - } + // if (this.enabled) { + // // 渲染前根据 viewport 调整 FBO size + // const { width, height } = rendererService.getViewportSize(); + // layer.multiPassRenderer.resize(width, height); + // } }); } @@ -103,26 +103,26 @@ export default class MultiPassRendererPlugin implements ILayerPlugin { } // use TAA pass if enabled instead of render pass - if (enableTAA) { - multiPassRenderer.add(normalPassFactory('taa')); - } else { - // render all layers in this pass - multiPassRenderer.add(normalPassFactory('render')); - } + // if (enableTAA) { + // multiPassRenderer.add(normalPassFactory('taa')); + // } else { + // // render all layers in this pass + // multiPassRenderer.add(normalPassFactory('render')); + // } // post processing - normalizePasses(passes).forEach( - (pass: [string, { [key: string]: unknown }]) => { - const [passName, initializationOptions] = pass; - multiPassRenderer.add( - postProcessingPassFactory(passName), - initializationOptions, - ); - }, - ); + // normalizePasses(passes).forEach( + // (pass: [string, { [key: string]: unknown }]) => { + // const [passName, initializationOptions] = pass; + // multiPassRenderer.add( + // postProcessingPassFactory(passName), + // initializationOptions, + // ); + // }, + // ); // 末尾为固定的 CopyPass - multiPassRenderer.add(postProcessingPassFactory('copy')); + // multiPassRenderer.add(postProcessingPassFactory('copy')); return multiPassRenderer; } diff --git a/packages/layers/src/plugins/UpdateStyleAttributePlugin.ts b/packages/layers/src/plugins/UpdateStyleAttributePlugin.ts index 71fd3cf77f..6d091ce6a3 100644 --- a/packages/layers/src/plugins/UpdateStyleAttributePlugin.ts +++ b/packages/layers/src/plugins/UpdateStyleAttributePlugin.ts @@ -51,7 +51,11 @@ export default class UpdateStyleAttributePlugin implements ILayerPlugin { ) { const attributes = styleAttributeService.getLayerStyleAttributes() || []; const filter = styleAttributeService.getLayerStyleAttribute('filter'); - if (filter && filter.needRegenerateVertices) { + const shape = styleAttributeService.getLayerStyleAttribute('shape'); + if ( + (filter && filter.needRegenerateVertices) || + (shape && shape.needRegenerateVertices) // TODO:Shape 更新重新build + ) { layer.layerModelNeedUpdate = true; attributes.forEach((attr) => (attr.needRegenerateVertices = false)); return; diff --git a/packages/layers/src/point/models/fill.ts b/packages/layers/src/point/models/fill.ts index 35aa64a9e5..da221ea52c 100644 --- a/packages/layers/src/point/models/fill.ts +++ b/packages/layers/src/point/models/fill.ts @@ -18,6 +18,7 @@ interface IPointLayerStyleOptions { opacity: number; strokeWidth: number; stroke: string; + strokeOpacity: number; } export default class FillModel extends BaseModel { public getUninforms(): IModelUniform { @@ -25,11 +26,13 @@ export default class FillModel extends BaseModel { opacity = 1, stroke = 'rgb(0,0,0,0)', strokeWidth = 1, + strokeOpacity = 1, } = this.layer.getLayerConfig() as IPointLayerStyleOptions; return { u_opacity: opacity, u_stroke_width: strokeWidth, u_stroke_color: rgb2arr(stroke), + u_stroke_opacity: strokeOpacity, }; } public getAnimateUniforms(): IModelUniform { @@ -84,7 +87,7 @@ export default class FillModel extends BaseModel { vertex: number[], attributeIdx: number, ) => { - const extrude = [-1, -1, 1, -1, 1, 1, -1, 1]; + const extrude = [1, 1, -1, 1, -1, -1, 1, -1]; const extrudeIndex = (attributeIdx % 4) * 2; return [extrude[extrudeIndex], extrude[extrudeIndex + 1]]; }, diff --git a/packages/layers/src/point/shaders/fill_vert.glsl b/packages/layers/src/point/shaders/fill_vert.glsl index cf0e996ea2..6482ae442c 100644 --- a/packages/layers/src/point/shaders/fill_vert.glsl +++ b/packages/layers/src/point/shaders/fill_vert.glsl @@ -24,7 +24,7 @@ void main() { // radius(16-bit) v_radius = a_Size; - vec2 offset = project_pixel(extrude * (a_Size + u_stroke_width)); + vec2 offset = project_pixel(extrude * (a_Size + u_stroke_width)) * -1.; vec4 project_pos = project_position(vec4(a_Position.xy, 0.0, 1.0)); gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy + offset, 0.0, 1.0)); diff --git a/packages/layers/src/raster/raster.ts b/packages/layers/src/raster/raster.ts index c91f142845..6d2fb75c92 100644 --- a/packages/layers/src/raster/raster.ts +++ b/packages/layers/src/raster/raster.ts @@ -57,19 +57,7 @@ export default class RasterLayer extends BaseLayer { }); this.models = [this.buildRasterModel()]; } - protected getConfigSchema() { - return { - properties: { - opacity: { - type: 'number', - minimum: 0, - maximum: 1, - }, - }, - }; - } - - protected renderModels() { + public renderModels() { const { opacity, heightRatio = 10 } = this.getLayerConfig(); const parserDataItem = this.getSource().data.dataArray[0]; const { coordinates, width, height, min, max } = parserDataItem; @@ -91,6 +79,18 @@ export default class RasterLayer extends BaseLayer { return this; } + protected getConfigSchema() { + return { + properties: { + opacity: { + type: 'number', + minimum: 0, + maximum: 1, + }, + }, + }; + } + private buildRasterModel() { const source = this.getSource(); const sourceFeature = source.data.dataArray[0]; diff --git a/packages/layers/src/raster/raster2d.ts b/packages/layers/src/raster/raster2d.ts index e7f26d8093..bb2074c54f 100644 --- a/packages/layers/src/raster/raster2d.ts +++ b/packages/layers/src/raster/raster2d.ts @@ -57,20 +57,7 @@ export default class Raster2dLayer extends BaseLayer { }), ]; } - - protected getConfigSchema() { - return { - properties: { - opacity: { - type: 'number', - minimum: 0, - maximum: 1, - }, - }, - }; - } - - protected renderModels() { + public renderModels() { const { opacity } = this.getLayerConfig(); const parserDataItem = this.getSource().data.dataArray[0]; const { min, max } = parserDataItem; @@ -91,6 +78,18 @@ export default class Raster2dLayer extends BaseLayer { return this; } + protected getConfigSchema() { + return { + properties: { + opacity: { + type: 'number', + minimum: 0, + maximum: 1, + }, + }, + }; + } + private registerBuiltinAttributes() { // point layer size; this.styleAttributeService.registerStyleAttribute({ diff --git a/packages/maps/package.json b/packages/maps/package.json index f9e15b5ad4..394c244ec9 100644 --- a/packages/maps/package.json +++ b/packages/maps/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-maps", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -23,8 +23,8 @@ "author": "xiaoiver", "license": "ISC", "dependencies": { - "@antv/l7-core": "^2.0.22", - "@antv/l7-utils": "^2.0.22", + "@antv/l7-core": "^2.0.23-alpha.0", + "@antv/l7-utils": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "gl-matrix": "^3.1.0", "inversify": "^5.0.1", diff --git a/packages/maps/src/amap/map.ts b/packages/maps/src/amap/map.ts index 59b2af238f..5cd103d55e 100644 --- a/packages/maps/src/amap/map.ts +++ b/packages/maps/src/amap/map.ts @@ -240,6 +240,7 @@ export default class AMapService if (mapInstance) { this.map = mapInstance as AMap.Map & IAMapInstance; this.$mapContainer = this.map.getContainer(); + this.removeLogoControl(); setTimeout(() => { this.map.on('camerachange', this.handleCameraChanged); resolve(); @@ -255,6 +256,10 @@ export default class AMapService viewMode: '3D', ...rest, }); + map.on('complete', () => { + this.removeLogoControl(); + }); + // 监听地图相机事件 map.on('camerachange', this.handleCameraChanged); // @ts-ignore @@ -390,4 +395,12 @@ export default class AMapService document.head.appendChild(script); }); } + + private removeLogoControl(): void { + // @ts-ignore + const logo = document.getElementsByClassName('amap-logo'); + if (logo) { + logo[0].setAttribute('style', 'display: none !important'); + } + } } diff --git a/packages/maps/src/mapbox/theme.ts b/packages/maps/src/mapbox/theme.ts index c9940fdc77..9ef9086ac2 100644 --- a/packages/maps/src/mapbox/theme.ts +++ b/packages/maps/src/mapbox/theme.ts @@ -10,14 +10,6 @@ export const MapTheme: { glyphs: 'https://gw.alipayobjects.com/os/antvdemo/assets/mapbox/glyphs/{fontstack}/{range}.pbf', sources: {}, - layers: [ - { - id: 'background', - type: 'background', - paint: { - 'background-color': 'white', - }, - }, - ], + layers: [], }, }; diff --git a/packages/react/package.json b/packages/react/package.json index dc1c030166..89edb13bf7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-react", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -24,10 +24,12 @@ "author": "lzxue", "license": "ISC", "dependencies": { - "@antv/l7": "^2.0.22", - "@antv/l7-maps": "^2.0.22", + "@antv/l7": "^2.0.23-alpha.0", + "@antv/l7-maps": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", - "react": "^16.8.6" + "react": "^16.8.6", + "react-dom": "^16.10.2", + "load-styles": "^2.0.0" }, "gitHead": "f2bd3c6473df79d815467b1677c6f985cf68800e", "publishConfig": { diff --git a/packages/react/src/component/AMapScene.tsx b/packages/react/src/component/AMapScene.tsx new file mode 100644 index 0000000000..26048bd750 --- /dev/null +++ b/packages/react/src/component/AMapScene.tsx @@ -0,0 +1,51 @@ +import { IMapConfig, Scene } from '@antv/l7'; +// @ts-ignore +// tslint:disable-next-line:no-submodule-imports +import GaodeMap from '@antv/l7-maps/lib/amap'; +import React, { createElement, createRef, useEffect, useState } from 'react'; +import { SceneContext } from './SceneContext'; +interface IMapSceneConig { + style?: React.CSSProperties; + className?: string; + map: IMapConfig; + children?: JSX.Element | JSX.Element[] | Array; +} +const AMapScene = React.memo((props: IMapSceneConig) => { + const { style, className, map } = props; + const container = createRef(); + const [scene, setScene] = useState(); + useEffect(() => { + const sceneInstance = new Scene({ + id: container.current as HTMLDivElement, + map: new GaodeMap(map), + }); + sceneInstance.on('loaded', () => { + setScene(sceneInstance); + }); + return () => { + sceneInstance.destroy(); + }; + }, []); + useEffect(() => { + if (!scene) { + return; + } + scene.setMapStyle(map.style); + }, [map.style]); + + return ( + + {createElement( + 'div', + { + ref: container, + style, + className, + }, + scene && props.children, + )} + + ); +}); + +export default AMapScene; diff --git a/packages/react/src/component/Control.tsx b/packages/react/src/component/Control.tsx new file mode 100644 index 0000000000..776e402a12 --- /dev/null +++ b/packages/react/src/component/Control.tsx @@ -0,0 +1,39 @@ +import { IControl, Logo, PositionType, Scale, Scene, Zoom } from '@antv/l7'; +import React, { useEffect, useState } from 'react'; +import { useSceneValue } from './SceneContext'; +interface IControlProps { + type: 'scale' | 'zoom' | 'logo'; + position: PositionType; + [key: string]: any; +} +export default React.memo(function MapControl(props: IControlProps) { + const scene = (useSceneValue() as unknown) as Scene; + const [, setControl] = useState(); + const { type, position } = props; + useEffect(() => { + let ctr: IControl; + switch (type) { + case 'scale': + ctr = new Scale({ + position, + }); + break; + case 'zoom': + ctr = new Zoom({ + position, + }); + break; + case 'logo': + ctr = new Logo({ + position, + }); + } + setControl(ctr); + scene.addControl(ctr); + return () => { + scene.removeControl(ctr); + }; + }, [type, position]); + + return null; +}); diff --git a/packages/react/src/component/CustomControl.tsx b/packages/react/src/component/CustomControl.tsx new file mode 100644 index 0000000000..08f73e0193 --- /dev/null +++ b/packages/react/src/component/CustomControl.tsx @@ -0,0 +1,44 @@ +import { Control, PositionName, Scene } from '@antv/l7'; +import * as React from 'react'; +import { createPortal } from 'react-dom'; +import { useSceneValue } from './SceneContext'; +const { useEffect, useState } = React; + +interface IColorLegendProps { + position: PositionName; + className?: string; + style?: React.CSSProperties; + children?: JSX.Element | JSX.Element[] | Array; +} + +export default function CustoonConrol(props: IColorLegendProps) { + const { className, style, children, position } = props; + const mapScene = (useSceneValue() as unknown) as Scene; + const el = document.createElement('div'); + useEffect(() => { + const custom = new Control({ + position, + }); + custom.onAdd = () => { + if (className) { + el.className = className; + } + if (style) { + const cssText = Object.keys(style) + .map((key: string) => { + // @ts-ignore + return `${key}:${style[key]}`; + }) + .join(';'); + el.style.cssText = cssText; + } + + return el; + }; + mapScene.addControl(custom); + return () => { + mapScene.removeControl(custom); + }; + }, []); + return createPortal(children, el); +} diff --git a/packages/react/src/component/LayerAttribute/Color.tsx b/packages/react/src/component/LayerAttribute/Color.tsx index 98a78dfccc..e851c4835a 100644 --- a/packages/react/src/component/LayerAttribute/Color.tsx +++ b/packages/react/src/component/LayerAttribute/Color.tsx @@ -12,7 +12,8 @@ export default React.memo(function Chart(props: ILayerProps) { useEffect(() => { color.field ? layer.color(color.field as StyleAttrField, color.values) - : layer.color(color.value as StyleAttrField); - }, [color.value, color.field, JSON.stringify(color.values), color.values]); + : layer.color(color.values as StyleAttrField); + }, [color.field, color.scale, JSON.stringify(color.values)]); + return null; }); diff --git a/packages/react/src/component/LayerAttribute/Layer.tsx b/packages/react/src/component/LayerAttribute/Layer.tsx index 2d59ccffbb..c64ef9ee80 100644 --- a/packages/react/src/component/LayerAttribute/Layer.tsx +++ b/packages/react/src/component/LayerAttribute/Layer.tsx @@ -2,13 +2,12 @@ import { ILayer, LineLayer, PointLayer, PolygonLayer, Scene } from '@antv/l7'; import * as React from 'react'; import { LayerContext } from '../LayerContext'; import { useSceneValue } from '../SceneContext'; - import { Active, Color, Filter, ILayerProps, - Scales, + Scale, Shape, Size, Source, @@ -17,17 +16,14 @@ import { const { useEffect, useState } = React; -export default function BaseLayer( - type: string, - props: ILayerProps & { children?: any }, -) { +export default function BaseLayer(type: string, props: ILayerProps) { const { source, color, shape, style, size, - scales, + scale, active, filter, options, @@ -66,7 +62,7 @@ export default function BaseLayer( return ( - {scales && } + {scale && } {size && } diff --git a/packages/react/src/component/LayerAttribute/Scales.tsx b/packages/react/src/component/LayerAttribute/Scale.tsx similarity index 52% rename from packages/react/src/component/LayerAttribute/Scales.tsx rename to packages/react/src/component/LayerAttribute/Scale.tsx index 1adcb2e41a..5a48f9e7f1 100644 --- a/packages/react/src/component/LayerAttribute/Scales.tsx +++ b/packages/react/src/component/LayerAttribute/Scale.tsx @@ -5,14 +5,12 @@ import { IScaleAttributeOptions } from './'; const { useEffect } = React; interface ILayerProps { layer: ILayer; - scales: Partial; + scale: Partial; } export default React.memo(function Chart(props: ILayerProps) { - const { layer, scales } = props; + const { layer, scale } = props; useEffect(() => { - scales.field - ? layer.scale(scales.field as string, scales.value as IScale) - : layer.scale(scales.values as IScaleOptions); - }, [scales.value, scales.field, JSON.stringify(scales.values)]); + layer.scale(scale.values as IScaleOptions); + }, [scale.values]); return null; }); diff --git a/packages/react/src/component/LayerAttribute/Shape.tsx b/packages/react/src/component/LayerAttribute/Shape.tsx index 7ae9a8048b..cb39018d70 100644 --- a/packages/react/src/component/LayerAttribute/Shape.tsx +++ b/packages/react/src/component/LayerAttribute/Shape.tsx @@ -12,7 +12,7 @@ export default React.memo(function Chart(props: ILayerProps) { useEffect(() => { shape.field ? layer.shape(shape.field, shape.values) - : layer.shape(shape.value as StyleAttrField); - }, [shape.field, shape.value, JSON.stringify(shape.values), shape.values]); + : layer.shape(shape.values as StyleAttrField); + }, [shape.field, JSON.stringify(shape.values)]); return null; }); diff --git a/packages/react/src/component/LayerAttribute/Size.tsx b/packages/react/src/component/LayerAttribute/Size.tsx index 9065092ef0..92671a4fe4 100644 --- a/packages/react/src/component/LayerAttribute/Size.tsx +++ b/packages/react/src/component/LayerAttribute/Size.tsx @@ -12,7 +12,7 @@ export default React.memo(function Chart(props: ILayerProps) { useEffect(() => { size.field ? layer.size(size.field, size.values) - : layer.size(size.value as StyleAttrField); - }, [size.field, size.value, JSON.stringify(size.values), size.values]); + : layer.size(size.values as StyleAttrField); + }, [size.field, size.values, size.scale]); return null; }); diff --git a/packages/react/src/component/LayerAttribute/Source.tsx b/packages/react/src/component/LayerAttribute/Source.tsx index a3fe84b07c..edccf83784 100644 --- a/packages/react/src/component/LayerAttribute/Source.tsx +++ b/packages/react/src/component/LayerAttribute/Source.tsx @@ -16,11 +16,7 @@ export default React.memo(function Chart(props: ISourceProps) { layer.source(data, sourceOption); } else { layer.setData(data, sourceOption); - if (layer.type === 'PolygonLayer') { - // 重新设置data之后,自适应处理 - layer.fitBounds(); - } } - }, [data]); + }, [data, sourceOption]); return null; }); diff --git a/packages/react/src/component/LayerAttribute/index.ts b/packages/react/src/component/LayerAttribute/index.ts index c0de44a2be..8756ebef60 100644 --- a/packages/react/src/component/LayerAttribute/index.ts +++ b/packages/react/src/component/LayerAttribute/index.ts @@ -2,7 +2,7 @@ import { IActiveOption, IScale, IScaleOptions, ISourceCFG } from '@antv/l7'; import Active from './Active'; import Color from './Color'; import Filter from './Filter'; -import Scales from './Scales'; +import Scale from './Scale'; import Shape from './Shape'; import Size from './Size'; import Source from './Source'; @@ -12,14 +12,19 @@ type CallBack = (...args: any[]) => any; export interface IAttributeOptions { field: string; - value: string | number | CallBack; - values: string[] | number[] | string | CallBack; + value: string | number; + values: string[] | number[] | string | number; + scale?: string; } export interface IScaleAttributeOptions { - field: string; + field: string | IScaleOptions; value: IScale; - values: IScaleOptions; + values: IScaleOptions | IScale; +} + +export interface IScaleOption { + [key: string]: IScaleAttributeOptions; } export interface IStyleOptions { opacity: number; @@ -40,11 +45,12 @@ export interface ILayerProps { source: ISourceOptions; color: Partial; shape: Partial; - scales?: Partial; + scale?: Partial; size?: Partial; style?: Partial; active?: IActiveOptions; filter?: Partial; + children?: JSX.Element | JSX.Element[] | Array; } -export { Active, Color, Filter, Source, Size, Shape, Style, Scales }; +export { Active, Color, Filter, Source, Size, Shape, Style, Scale }; diff --git a/packages/react/src/component/Legend/color.tsx b/packages/react/src/component/Legend/color.tsx new file mode 100644 index 0000000000..5b1cae9deb --- /dev/null +++ b/packages/react/src/component/Legend/color.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +interface IColorLegendProps { + items?: any[]; + title: string; + className?: string; + style?: React.CSSProperties; +} +import './style.css'; +export const ColorComponent = React.memo((props: IColorLegendProps) => { + const { className, style, title } = props; + const items = [ + { title: '1', color: 'rgb(239,243,255)' }, + { title: '10', color: 'rgb(198,219,239)' }, + { title: '30', color: 'rgb(158,202,225)' }, + { title: '50', color: 'rgb(107,174,214)' }, + { title: '60', color: 'rgb(49,130,189)' }, + { title: '100', color: 'rgb(8,81,156)' }, + ]; + + return ( +
+
+ {items.map((c, i) => { + return ( +
+ ); + })} +
+
+ {items.map((c, i) => { + return ( +
+ {c.title} +
+ ); + })} +
+
+ ); +}); diff --git a/packages/react/src/component/Legend/style.css b/packages/react/src/component/Legend/style.css new file mode 100644 index 0000000000..244c126247 --- /dev/null +++ b/packages/react/src/component/Legend/style.css @@ -0,0 +1,3 @@ +.color { + color:'red'; +} diff --git a/packages/react/src/component/MapboxScene.tsx b/packages/react/src/component/MapboxScene.tsx new file mode 100644 index 0000000000..de11fdfe9b --- /dev/null +++ b/packages/react/src/component/MapboxScene.tsx @@ -0,0 +1,51 @@ +import { IMapConfig, Scene } from '@antv/l7'; +// @ts-ignore +// tslint:disable-next-line:no-submodule-imports +import Mapbox from '@antv/l7-maps/lib/mapbox'; +import React, { createElement, createRef, useEffect, useState } from 'react'; +import { SceneContext } from './SceneContext'; +interface IMapSceneConig { + style?: React.CSSProperties; + className?: string; + map: IMapConfig; + children?: JSX.Element | JSX.Element[] | Array; +} +const MapboxScene = React.memo((props: IMapSceneConig) => { + const { style, className, map } = props; + const container = createRef(); + const [scene, setScene] = useState(); + useEffect(() => { + const sceneInstance = new Scene({ + id: container.current as HTMLDivElement, + map: new Mapbox(map), + }); + sceneInstance.on('loaded', () => { + setScene(sceneInstance); + }); + return () => { + sceneInstance.destroy(); + }; + }, []); + useEffect(() => { + if (!scene) { + return; + } + scene.setMapStyle(map.style); + }, [map.style]); + + return ( + + {createElement( + 'div', + { + ref: container, + style, + className, + }, + scene && props.children, + )} + + ); +}); + +export default MapboxScene; diff --git a/packages/react/src/component/Scene.tsx b/packages/react/src/component/Scene.tsx index a56b7e205f..134d5c6f1b 100644 --- a/packages/react/src/component/Scene.tsx +++ b/packages/react/src/component/Scene.tsx @@ -36,6 +36,12 @@ const MapScene = React.memo((props: IMapSceneConig) => { sceneInstance.destroy(); }; }, []); + useEffect(() => { + if (!scene) { + return; + } + scene.setMapStyle(style); + }, [style]); return ( diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 70c6d57841..a8cb197e42 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,5 +1,13 @@ +export * from './component/SceneContext'; +export { default as Scene } from './component/Scene'; +export { default as AMapScene } from './component/AMapScene'; +export { default as MapboxScene } from './component/MapboxScene'; +export * from './component/Layer'; +export { default as Control } from './component/Control'; +export { default as CustomControl } from './component/CustomControl'; export { MapScene } from './component/Scene'; export { PolygonLayer, LineLayer, PointLayer } from './component/Layer'; export { LayerEvent } from './component/LayerEvent'; export { useSceneValue, SceneContext } from './component/SceneContext'; export { useLayerValue, LayerContext } from './component/LayerContext'; +export { ColorComponent } from './component/Legend/color'; diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 2b076a1a92..d17435637b 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-renderer", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -25,7 +25,7 @@ "gl": "^4.4.0" }, "dependencies": { - "@antv/l7-core": "^2.0.22", + "@antv/l7-core": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "inversify": "^5.0.1", "lodash": "^4.17.15", diff --git a/packages/scene/package.json b/packages/scene/package.json index 6facf5f654..cc80310ad8 100644 --- a/packages/scene/package.json +++ b/packages/scene/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-scene", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -22,11 +22,11 @@ "author": "xiaoiver", "license": "ISC", "dependencies": { - "@antv/l7-component": "^2.0.22", - "@antv/l7-core": "^2.0.22", - "@antv/l7-maps": "^2.0.22", - "@antv/l7-renderer": "^2.0.22", - "@antv/l7-utils": "^2.0.22", + "@antv/l7-component": "^2.0.23-alpha.0", + "@antv/l7-core": "^2.0.23-alpha.0", + "@antv/l7-maps": "^2.0.23-alpha.0", + "@antv/l7-renderer": "^2.0.23-alpha.0", + "@antv/l7-utils": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "inversify": "^5.0.1", "mapbox-gl": "^1.2.1", diff --git a/packages/source/package.json b/packages/source/package.json index fb913ce491..52d7c33247 100644 --- a/packages/source/package.json +++ b/packages/source/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-source", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", @@ -24,8 +24,8 @@ "author": "lzxue", "license": "ISC", "dependencies": { - "@antv/l7-core": "^2.0.22", - "@antv/l7-utils": "^2.0.22", + "@antv/l7-core": "^2.0.23-alpha.0", + "@antv/l7-utils": "^2.0.23-alpha.0", "@babel/runtime": "^7.7.7", "@mapbox/geojson-rewind": "^0.4.0", "@turf/helpers": "^6.1.4", diff --git a/packages/source/src/transform/cluster.ts b/packages/source/src/transform/cluster.ts index 2d4c9ae1e1..edcf9e585d 100644 --- a/packages/source/src/transform/cluster.ts +++ b/packages/source/src/transform/cluster.ts @@ -3,7 +3,10 @@ import Supercluster from 'supercluster'; export function cluster(data: IParserData, option: ITransform): IParserData { const { radius = 80, maxZoom = 18, minZoom = 0, field, zoom = 2 } = option; if (data.pointIndex) { - const clusterData = data.pointIndex.getClusters(data.extent, zoom); + const clusterData = data.pointIndex.getClusters( + data.extent, + Math.floor(zoom), + ); data.dataArray = formatData(clusterData); return data; } diff --git a/packages/utils/package.json b/packages/utils/package.json index db8c657d71..80c1d0e759 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7-utils", - "version": "2.0.22", + "version": "2.0.23-alpha.0", "description": "", "main": "lib/index.js", "module": "es/index.js", diff --git a/stories/Components/components/Scale.tsx b/stories/Components/components/Scale.tsx index 6255ed124f..c474d43923 100644 --- a/stories/Components/components/Scale.tsx +++ b/stories/Components/components/Scale.tsx @@ -62,7 +62,7 @@ export default class ScaleComponent extends React.Component { }) .size('point_count', [5, 10, 15, 20, 25]) .animate(false) - .select(true) + .active(true) .color('yellow') .style({ opacity: 0.5, diff --git a/stories/Layers/Layers.stories.tsx b/stories/Layers/Layers.stories.tsx index 7f8f130e9d..9bb6bc3b64 100644 --- a/stories/Layers/Layers.stories.tsx +++ b/stories/Layers/Layers.stories.tsx @@ -14,6 +14,7 @@ import LineLayer from './components/Line'; import PointDemo from './components/Point'; import Point3D from './components/Point3D'; import PointImage from './components/PointImage'; +import PolygonDemo from './components/polygon'; import Polygon3D from './components/Polygon3D'; import ImageLayerDemo from './components/RasterImage'; import RasterLayerDemo from './components/RasterLayer'; @@ -30,6 +31,7 @@ storiesOf('图层', module) .add('Column', () => ) .add('图片标注', () => ) .add('面3d图层', () => ) + .add('面图层', () => ) .add('点亮城市', () => ) .add('线图层', () => ) .add('虚线', () => ) diff --git a/stories/Layers/components/Point.tsx b/stories/Layers/components/Point.tsx index e4628ee7b9..8745065282 100644 --- a/stories/Layers/components/Point.tsx +++ b/stories/Layers/components/Point.tsx @@ -19,40 +19,50 @@ export default class Point3D extends React.Component { const scene = new Scene({ id: document.getElementById('map') as HTMLDivElement, - map: new Mapbox({ + map: new GaodeMap({ center: [120.19382669582967, 30.258134], pitch: 0, style: 'dark', zoom: 0, }), }); - // scene.on('loaded', () => { - const pointLayer = new PointLayer({}) - .source(pointsData, { - cluster: false, - }) - .shape('circle') - // .scale('point_count', { - // type: 'quantile', - // }) - .size('mag', [5, 10, 15, 20, 25]) - .animate(false) - .active(true) - .color('yellow') - .style({ - opacity: 0.5, - strokeWidth: 1, - }); - scene.addLayer(pointLayer); scene.on('loaded', () => { - const newData = { - type: 'FeatureCollection', - features: pointsData.features.slice(0, 100), - }; - pointLayer.setData(newData); + const pointLayer = new PointLayer({}) + .source(pointsData, { + cluster: false, + }) + .scale({ + size: { + type: 'power', + field: 'mag', + }, + color: { + type: 'linear', + field: 'mag', + }, + }) + .shape('circle') + .size('mag', [2, 8, 14, 20, 26, 32, 40]) + .animate(false) + .active(true) + .color('mag', ['red', 'blue', 'yellow', 'green']) + .style({ + opacity: 0.5, + strokeWidth: 1, + }); + scene.addLayer(pointLayer); + const items = pointLayer.getLegendItems('color'); + console.log(items); + console.log(pointLayer.getLegendItems('size')); + // scene.on('loaded', () => { + // const newData = { + // type: 'FeatureCollection', + // features: pointsData.features.slice(0, 100), + // }; + // pointLayer.setData(newData); + // }); + this.scene = scene; }); - this.scene = scene; - // }); } public render() { diff --git a/stories/Layers/components/PointImage.tsx b/stories/Layers/components/PointImage.tsx index 4af0192c04..35c9222604 100644 --- a/stories/Layers/components/PointImage.tsx +++ b/stories/Layers/components/PointImage.tsx @@ -35,22 +35,24 @@ export default class PointImage extends React.Component { '02', 'https://gw.alipayobjects.com/mdn/antv_site/afts/img/A*o16fSIvcKdUAAAAAAAAAAABkARQnAQ', ); - - const imageLayer = new PointLayer({}) - .source(await response.json(), { - parser: { - type: 'json', - x: 'longitude', - y: 'latitude', - }, - }) - .shape('name', ['00', '01', '02']) - .active(true) - .size(30); - scene.addLayer(imageLayer); - imageLayer.on('click', (e) => { - console.log(e); - }); + let i = 0; + const data = await response.json(); + while (i < 50) { + const imageLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'longitude', + y: 'latitude', + }, + }) + .shape('triangle') + .color('red') + .active(true) + .size(20); + scene.addLayer(imageLayer); + i++; + } } public render() { diff --git a/stories/Layers/components/Text.tsx b/stories/Layers/components/Text.tsx index cf994f63e8..98de3972fd 100644 --- a/stories/Layers/components/Text.tsx +++ b/stories/Layers/components/Text.tsx @@ -39,7 +39,7 @@ export default class TextLayerDemo extends React.Component { y: 'w', }, }) - .shape('m', 'text') + .shape('w', 'text') // .shape('circle') .size(12) .filter('t', (t) => { @@ -63,7 +63,7 @@ export default class TextLayerDemo extends React.Component { const gui = new dat.GUI(); this.gui = gui; const styleOptions = { - textAnchor: 'center', + field: 'w', strokeWidth: 1, textAllowOverlap: false, opacity: 1, @@ -71,21 +71,10 @@ export default class TextLayerDemo extends React.Component { }; const rasterFolder = gui.addFolder('文本可视化'); rasterFolder - .add(styleOptions, 'textAnchor', [ - 'center', - 'left', - 'right', - 'top', - 'bottom', - 'top-left', - 'bottom-right', - 'bottom-left', - 'top-right', - ]) + .add(styleOptions, 'field', ['w', 's', 'l', 'm', 'j', 'h']) .onChange((anchor: string) => { - pointLayer.style({ - textAnchor: anchor, - }); + console.log(anchor); + pointLayer.shape(anchor, 'text'); scene.render(); }); diff --git a/stories/Layers/components/layertest.tsx b/stories/Layers/components/layertest.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/stories/Layers/components/polygon.tsx b/stories/Layers/components/polygon.tsx new file mode 100644 index 0000000000..c322d0ec7c --- /dev/null +++ b/stories/Layers/components/polygon.tsx @@ -0,0 +1,190 @@ +import { PolygonLayer, Scene } from '@antv/l7'; +import { GaodeMap, Mapbox } from '@antv/l7-maps'; +import * as dat from 'dat.gui'; +import * as React from 'react'; +// @ts-ignore +const Spectral: { + [key: string]: string[]; +} = { + 3: ['rgb(252,141,89)', 'rgb(255,255,191)', 'rgb(153,213,148)'], + 4: [ + 'rgb(215,25,28)', + 'rgb(253,174,97)', + 'rgb(171,221,164)', + 'rgb(43,131,186)', + ], + 5: [ + 'rgb(215,25,28)', + 'rgb(253,174,97)', + 'rgb(255,255,191)', + 'rgb(171,221,164)', + 'rgb(43,131,186)', + ], + 6: [ + 'rgb(213,62,79)', + 'rgb(252,141,89)', + 'rgb(254,224,139)', + 'rgb(230,245,152)', + 'rgb(153,213,148)', + 'rgb(50,136,189)', + ], + 7: [ + 'rgb(213,62,79)', + 'rgb(252,141,89)', + 'rgb(254,224,139)', + 'rgb(255,255,191)', + 'rgb(230,245,152)', + 'rgb(153,213,148)', + 'rgb(50,136,189)', + ], + 8: [ + 'rgb(213,62,79)', + 'rgb(244,109,67)', + 'rgb(253,174,97)', + 'rgb(254,224,139)', + 'rgb(230,245,152)', + 'rgb(171,221,164)', + 'rgb(102,194,165)', + 'rgb(50,136,189)', + ], + 9: [ + 'rgb(213,62,79)', + 'rgb(244,109,67)', + 'rgb(253,174,97)', + 'rgb(254,224,139)', + 'rgb(255,255,191)', + 'rgb(230,245,152)', + 'rgb(171,221,164)', + 'rgb(102,194,165)', + 'rgb(50,136,189)', + ], + 10: [ + 'rgb(158,1,66)', + 'rgb(213,62,79)', + 'rgb(244,109,67)', + 'rgb(253,174,97)', + 'rgb(254,224,139)', + 'rgb(230,245,152)', + 'rgb(171,221,164)', + 'rgb(102,194,165)', + 'rgb(50,136,189)', + 'rgb(94,79,162)', + ], + 11: [ + 'rgb(158,1,66)', + 'rgb(213,62,79)', + 'rgb(244,109,67)', + 'rgb(253,174,97)', + 'rgb(254,224,139)', + 'rgb(255,255,191)', + 'rgb(230,245,152)', + 'rgb(171,221,164)', + 'rgb(102,194,165)', + 'rgb(50,136,189)', + 'rgb(94,79,162)', + ], +}; +export default class TextLayerDemo extends React.Component { + // @ts-ignore + private scene: Scene; + private gui: dat.GUI; + + public componentWillUnmount() { + this.scene.destroy(); + if (this.gui) { + this.gui.destroy(); + } + } + public async componentDidMount() { + const response = await fetch( + 'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json', + ); + const data = await response.json(); + + const scene = new Scene({ + id: 'map', + map: new Mapbox({ + center: [120.19382669582967, 30.258134], + pitch: 0, + style: 'dark', + zoom: 3, + }), + }); + scene.on('loaded', () => { + const layer = new PolygonLayer({}) + .source(data) + .shape('fill') + .color('childrenNum', [ + 'rgb(247,252,240)', + 'rgb(224,243,219)', + 'rgb(204,235,197)', + 'rgb(168,221,181)', + 'rgb(123,204,196)', + 'rgb(78,179,211)', + 'rgb(43,140,190)', + 'rgb(8,88,158)', + ]) + .style({ + opacity: 1.0, + }); + scene.addLayer(layer); + this.scene = scene; + + const gui = new dat.GUI(); + this.gui = gui; + const styleOptions = { + textAnchor: 'center', + filter: 1, + textAllowOverlap: 4, + opacity: 1, + color: '#ffffff', + }; + const rasterFolder = gui.addFolder('面图层可视化'); + + rasterFolder + .add(styleOptions, 'filter', 0, 50) + .onChange((strokeWidth: number) => { + layer.filter('childrenNum', (t: number) => { + return t > strokeWidth; + }); + scene.render(); + }); + rasterFolder + .add(styleOptions, 'textAllowOverlap', 3, 10, 1) + .onChange((v: string) => { + layer.color('childrenNum', Spectral[v]); + scene.render(); + }); + rasterFolder + .add(styleOptions, 'opacity', 0, 1) + .onChange((opacity: number) => { + layer.style({ + opacity, + }); + scene.render(); + setTimeout(() => { + scene.render(); + }, 10); + }); + rasterFolder.addColor(styleOptions, 'color').onChange((color: string) => { + layer.color(color); + scene.render(); + }); + }); + } + + public render() { + return ( +
+ ); + } +} diff --git a/stories/React/components/Scene.tsx b/stories/React/components/Scene.tsx index 396ffa9823..4f2595e02b 100644 --- a/stories/React/components/Scene.tsx +++ b/stories/React/components/Scene.tsx @@ -1,5 +1,5 @@ import { GaodeMap, Mapbox } from '@antv/l7-maps'; -import { LineLayer, Scene } from '@antv/l7-react'; +import { LineLayer, MapScene } from '@antv/l7-react'; import * as React from 'react'; export default React.memo(function Map() { @@ -23,7 +23,7 @@ export default React.memo(function Map() { }, []); return ( <> - )} - + ); }); diff --git a/yarn.lock b/yarn.lock index cb6dd9d42d..c912d33789 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22411,8 +22411,8 @@ table@^5.0.0, table@^5.2.3: tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" - resolved "https://registry.npm.taobao.org/tapable/download/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha1-ofzMBrWNth/XpF2i2kT186Pme6I= + resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tapable@^2.0.0-beta.8: version "2.0.0-beta.9"