From 41624f12fe291cd26197d38cbdd9424c6e050f77 Mon Sep 17 00:00:00 2001 From: thinkinggis Date: Fri, 17 Jan 2020 00:40:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(component):=20=E6=96=B0=E5=A2=9Ecluster=20?= =?UTF-8?q?marker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.zh-CN.md | 1 - docs/api/component/marker.en.md | 2 +- docs/api/component/marker.zh.md | 2 +- examples/point/marker/demo/marker.js | 2 +- packages/component/src/css/l7.css | 21 ++ packages/component/src/index.ts | 3 +- packages/component/src/markerlayer.ts | 201 ++++++++++++++++++ .../src/services/component/IMarkerService.ts | 14 ++ .../src/services/component/MarkerService.ts | 50 ++++- .../core/src/services/scene/SceneService.ts | 1 + packages/scene/src/index.ts | 9 + site/css/demo.css | 2 +- stories/Components/Components.stories.tsx | 6 +- .../Components/components/clusterMarker.tsx | 74 +++++++ stories/Components/components/markerlayer.tsx | 78 +++++++ 15 files changed, 458 insertions(+), 8 deletions(-) create mode 100644 packages/component/src/markerlayer.ts create mode 100644 stories/Components/components/clusterMarker.tsx create mode 100644 stories/Components/components/markerlayer.tsx diff --git a/README.zh-CN.md b/README.zh-CN.md index b07b70e5b4..6a38d0b49b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,6 @@ L7 能够满足常见的地图图表,BI 系统的可视化分析、以及 GIS ![l7 demo](https://gw.alipayobjects.com/mdn/rms_855bab/afts/img/A*S-73QpO8d0YAAAAAAAAAAABkARQnAQ) - ## 🌟 核心特性 🌏 数据驱动可视化展示 diff --git a/docs/api/component/marker.en.md b/docs/api/component/marker.en.md index bc6ee0bcf4..071271cd03 100644 --- a/docs/api/component/marker.en.md +++ b/docs/api/component/marker.en.md @@ -68,7 +68,7 @@ scene.addMarker(marker); ```javascript var el = document.createElement('label'); -el.className = 'lableclass'; +el.className = 'labelclass'; el.textContent = data[i].v; el.style.background = getColor(data[i].v); new L7.Marker({ diff --git a/docs/api/component/marker.zh.md b/docs/api/component/marker.zh.md index febdfd6c78..dcba499aa9 100644 --- a/docs/api/component/marker.zh.md +++ b/docs/api/component/marker.zh.md @@ -71,7 +71,7 @@ Marker ```javascript var el = document.createElement('label'); -el.className = 'lableclass'; +el.className = 'labelclass'; el.textContent = data[i].v; el.style.background = getColor(data[i].v); new L7.Marker({ diff --git a/examples/point/marker/demo/marker.js b/examples/point/marker/demo/marker.js index 71dc2456fe..f07b000b2a 100644 --- a/examples/point/marker/demo/marker.js +++ b/examples/point/marker/demo/marker.js @@ -24,7 +24,7 @@ function addMarkers() { continue; } const el = document.createElement('label'); - el.className = 'lableclass'; + el.className = 'labelclass'; el.textContent = nodes[i].v + '℃'; el.style.background = getColor(nodes[i].v); el.style.borderColor = getColor(nodes[i].v); diff --git a/packages/component/src/css/l7.css b/packages/component/src/css/l7.css index f20392e7c5..fee55c8779 100644 --- a/packages/component/src/css/l7.css +++ b/packages/component/src/css/l7.css @@ -11,6 +11,27 @@ z-index: 5; cursor: pointer; } + +.l7-marker-cluster { + background-clip: padding-box; + border-radius: 20px; + background-color: rgba(181, 226, 140, 0.6); + width: 40px; + height: 40px; +} +.l7-marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + background-color: rgba(110, 204, 57, 0.6); +} +.l7-marker-cluster span { + line-height: 30px; +} .l7-popup-anchor-top, .l7-popup-anchor-top-left, .l7-popup-anchor-top-right { diff --git a/packages/component/src/index.ts b/packages/component/src/index.ts index e006447f0f..8e991a7e6f 100644 --- a/packages/component/src/index.ts +++ b/packages/component/src/index.ts @@ -4,10 +4,11 @@ import Logo from './control/logo'; import Scale from './control/scale'; import Zoom from './control/zoom'; import Marker from './marker'; +import MarkerLayer from './markerlayer'; import Popup from './popup'; // 引入样式 // TODO: 使用 Less 或者 Sass,每个组件单独引用自身样式 import './css/l7.css'; -export { Control, Logo, Scale, Zoom, Layers, Marker, Popup }; +export { Control, Logo, Scale, Zoom, Layers, Marker, Popup, MarkerLayer }; diff --git a/packages/component/src/markerlayer.ts b/packages/component/src/markerlayer.ts new file mode 100644 index 0000000000..05e510744a --- /dev/null +++ b/packages/component/src/markerlayer.ts @@ -0,0 +1,201 @@ +import { IMapService, IMarker, TYPES } from '@antv/l7-core'; +import { bindAll, DOM } from '@antv/l7-utils'; +import { EventEmitter } from 'eventemitter3'; +import { Container } from 'inversify'; +import { isFunction } from 'lodash'; +import Supercluster from 'supercluster'; +import Marker from './marker'; +type CallBack = (...args: any[]) => any; +interface IMarkerStyleOption { + el: HTMLDivElement | string; + style: { [key: string]: any } | CallBack; + className: string; + radius: number; + maxZoom: number; + minZoom: number; + zoom: number; +} + +interface IMarkerLayerOption { + cluster: boolean; + clusterOption: IMarkerStyleOption; +} + +interface IPointFeature { + geometry: { + type: 'Point'; + coordinates: [number, number]; + }; + properties: any; +} +export default class MarkerLayer extends EventEmitter { + private markers: IMarker[] = []; + private markerLayerOption: IMarkerLayerOption; + private clusterIndex: Supercluster; + private points: IPointFeature[] = []; + private clusterMarkers: IMarker[] = []; + private mapsService: IMapService; + private scene: Container; + private zoom: number; + + constructor(option?: Partial) { + super(); + this.markerLayerOption = { + ...this.getDefault(), + ...option, + }; + bindAll(['update'], this); + this.zoom = this.markerLayerOption.clusterOption?.zoom || -99; + } + public getDefault() { + return { + cluster: false, + clusterOption: { + radius: 80, + maxZoom: 20, + minZoom: 0, + zoom: -99, + style: {}, + className: '', + el: '', + }, + }; + } + public addTo(scene: Container) { + // this.remove(); + this.scene = scene; + this.mapsService = scene.get(TYPES.IMapService); + this.initCluster(); + this.update(); + this.mapsService.on('zoom', this.update); + this.mapsService.on('zoomchange', this.update); + this.addMarkers(); + return this; + } + public addMarker(marker: IMarker) { + const cluster = this.markerLayerOption.cluster; + if (cluster) { + this.addPoint(marker); + } + this.markers.push(marker); + } + + public removeMarker(marker: IMarker) { + this.markers.indexOf(marker); + const markerIndex = this.markers.indexOf(marker); + if (markerIndex > -1) { + this.markers.splice(markerIndex, 1); + } + } + + public getMarkers() { + const cluster = this.markerLayerOption.cluster; + return cluster ? this.clusterMarkers : this.markers; + } + + public addMarkers() { + this.getMarkers().forEach((marker: IMarker) => { + marker.addTo(this.scene); + }); + } + public clear() { + this.markers.forEach((marker: IMarker) => { + marker.remove(); + }); + this.mapsService.off('zoomchange', this.update); + this.markers = []; + } + + public destroy() { + this.clear(); + this.removeAllListeners(); + } + + private addPoint(marker: IMarker) { + const { lng, lat } = marker.getLnglat(); + const feature: IPointFeature = { + geometry: { + type: 'Point', + coordinates: [lng, lat], + }, + properties: marker.getExtData(), + }; + this.points.push(feature); + } + + private initCluster() { + if (!this.markerLayerOption.cluster) { + return; + } + const { radius, minZoom = 0, maxZoom } = this.markerLayerOption + .clusterOption as IMarkerStyleOption; + this.clusterIndex = new Supercluster({ + radius, + minZoom, + maxZoom, + }); + // @ts-ignore + this.clusterIndex.load(this.points); + } + + private getClusterMarker(zoom: number) { + const clusterPoint = this.clusterIndex.getClusters( + [-180, -85, 180, 85], + zoom, + ); + this.clusterMarkers.forEach((marker: IMarker) => { + marker.remove(); + }); + this.clusterMarkers = []; + clusterPoint.forEach((feature) => { + const marker = + feature.properties && feature.properties.hasOwnProperty('point_count') + ? this.clusterMarker(feature) + : this.normalMarker(feature); + + this.clusterMarkers.push(marker); + marker.addTo(this.scene); + }); + } + private clusterMarker(feature: any) { + const clusterOption = this.markerLayerOption.clusterOption; + + const { className = '', style } = clusterOption as IMarkerStyleOption; + const el = DOM.create('div', 'l7-marker-cluster'); + const label = DOM.create('div', '', el); + const span = DOM.create('span', '', label); + if (className !== '') { + DOM.addClass(el, className); + } + span.textContent = feature.properties.point_count; + const elStyle = isFunction(style) + ? style(feature.properties.point_count) + : style; + + Object.keys(elStyle).forEach((key: string) => { + // @ts-ignore + el.style[key] = elStyle[key]; + }); + const marker = new Marker({ + element: el, + }).setLnglat({ + lng: feature.geometry.coordinates[0], + lat: feature.geometry.coordinates[1], + }); + return marker; + } + private normalMarker(feature: any) { + const marker = new Marker().setLnglat({ + lng: feature.geometry.coordinates[0], + lat: feature.geometry.coordinates[1], + }); + return marker; + } + private update() { + const zoom = this.mapsService.getZoom(); + if (Math.abs(zoom - this.zoom) > 1) { + this.getClusterMarker(Math.floor(zoom)); + this.zoom = Math.floor(zoom); + } + } +} diff --git a/packages/core/src/services/component/IMarkerService.ts b/packages/core/src/services/component/IMarkerService.ts index dfca6d7284..c0a3e097db 100644 --- a/packages/core/src/services/component/IMarkerService.ts +++ b/packages/core/src/services/component/IMarkerService.ts @@ -15,15 +15,29 @@ export interface IMarker { setLnglat(lngLat: ILngLat | IPoint): this; getLnglat(): ILngLat; getElement(): HTMLElement; + getExtData(): any; + setExtData(data: any): void; setPopup(popup: IPopup): void; togglePopup(): this; } export interface IMarkerService { container: HTMLElement; + addMarkerLayer(Marker: IMarkerLayer): void; + removeMarkerLayer(Marker: IMarkerLayer): void; addMarker(Marker: IMarker): void; addMarkers(): void; + addMarkerLayers(): void; removeMarker(Marker: IMarker): void; removeAllMarkers(): void; init(scene: Container): void; destroy(): void; } + +export interface IMarkerLayer { + addMarker(marker: IMarker): void; + getMarkers(): IMarker[]; + addTo(scene: Container): void; + removeMarker(marker: IMarker): void; + clear(): void; + destroy(): void; +} diff --git a/packages/core/src/services/component/MarkerService.ts b/packages/core/src/services/component/MarkerService.ts index dbc84de4ab..c16ff85a15 100644 --- a/packages/core/src/services/component/MarkerService.ts +++ b/packages/core/src/services/component/MarkerService.ts @@ -1,7 +1,12 @@ import { Container, injectable } from 'inversify'; import { TYPES } from '../../types'; import { IMapService } from '../map/IMapService'; -import { IMarker, IMarkerService, IMarkerServiceCfg } from './IMarkerService'; +import { + IMarker, + IMarkerLayer, + IMarkerService, + IMarkerServiceCfg, +} from './IMarkerService'; @injectable() export default class MarkerService implements IMarkerService { @@ -9,7 +14,28 @@ export default class MarkerService implements IMarkerService { private scene: Container; private mapsService: IMapService; private markers: IMarker[] = []; + private markerLayers: IMarkerLayer[] = []; private unAddMarkers: IMarker[] = []; + private unAddMarkerLayers: IMarkerLayer[] = []; + + public addMarkerLayer(markerLayer: IMarkerLayer): void { + if (this.mapsService.map && this.mapsService.getMarkerContainer()) { + this.markerLayers.push(markerLayer); + markerLayer.addTo(this.scene); + } else { + this.unAddMarkerLayers.push(markerLayer); + } + } + + public removeMarkerLayer(layer: IMarkerLayer): void { + layer.destroy(); + this.markerLayers.indexOf(layer); + const markerIndex = this.markerLayers.indexOf(layer); + if (markerIndex > -1) { + this.markerLayers.splice(markerIndex, 1); + } + } + public addMarker(marker: IMarker): void { if (this.mapsService.map && this.mapsService.getMarkerContainer()) { this.markers.push(marker); @@ -27,8 +53,21 @@ export default class MarkerService implements IMarkerService { this.unAddMarkers = []; } + public addMarkerLayers(): void { + this.unAddMarkerLayers.forEach((markerLayer: IMarkerLayer) => { + this.markerLayers.push(markerLayer); + markerLayer.addTo(this.scene); + }); + this.unAddMarkers = []; + } + public removeMarker(marker: IMarker): void { marker.remove(); + this.markers.indexOf(marker); + const markerIndex = this.markers.indexOf(marker); + if (markerIndex > -1) { + this.markers.splice(markerIndex, 1); + } } public removeAllMarkers(): void { @@ -44,5 +83,14 @@ export default class MarkerService implements IMarkerService { this.markers.forEach((marker: IMarker) => { marker.remove(); }); + this.markers = []; + this.markerLayers.forEach((layer: IMarkerLayer) => { + layer.destroy(); + }); + this.markerLayers = []; + } + + private removeMakerLayerMarker(layer: IMarkerLayer) { + layer.destroy(); } } diff --git a/packages/core/src/services/scene/SceneService.ts b/packages/core/src/services/scene/SceneService.ts index dc1064a0f6..7e02d130e7 100644 --- a/packages/core/src/services/scene/SceneService.ts +++ b/packages/core/src/services/scene/SceneService.ts @@ -141,6 +141,7 @@ export default class Scene extends EventEmitter implements ISceneService { this.map.addMarkerContainer(); // 初始化未加载的marker; this.markerService.addMarkers(); + this.markerService.addMarkerLayers(); // 地图初始化之后 才能初始化 container 上的交互 this.interactionService.init(); this.logger.debug('map loaded'); diff --git a/packages/scene/src/index.ts b/packages/scene/src/index.ts index ba8bc2fac7..b07ad3ec83 100644 --- a/packages/scene/src/index.ts +++ b/packages/scene/src/index.ts @@ -12,6 +12,7 @@ import { ILngLat, IMapService, IMarker, + IMarkerLayer, IMarkerService, IPoint, IPopup, @@ -163,6 +164,14 @@ class Scene this.markerService.addMarker(marker); } + public addMarkerLayer(layer: IMarkerLayer) { + this.markerService.addMarkerLayer(layer); + } + + public removeMarkerLayer(layer: IMarkerLayer) { + this.markerService.removeMarkerLayer(layer); + } + public removeAllMakers() { this.markerService.removeAllMarkers(); } diff --git a/site/css/demo.css b/site/css/demo.css index 7b6e46e0e3..b6e05780cf 100644 --- a/site/css/demo.css +++ b/site/css/demo.css @@ -1,4 +1,4 @@ -.lableclass { +.labelclass { position: absolute; display: inline; cursor: pointer; diff --git a/stories/Components/Components.stories.tsx b/stories/Components/Components.stories.tsx index 0c7bb0b105..bcfe8fc62d 100644 --- a/stories/Components/Components.stories.tsx +++ b/stories/Components/Components.stories.tsx @@ -1,7 +1,9 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; import Chart from './components/chart'; +import ClusterMarkerLayer from './components/clusterMarker'; import Marker from './components/Marker'; +import MarkerLayerComponent from './components/markerlayer'; import Popup from './components/Popup'; import Scale from './components/Scale'; import Zoom from './components/Zoom'; @@ -11,4 +13,6 @@ storiesOf('UI 组件', module) .add('Scale', () => ) .add('Marker', () => ) .add('Chart', () => ) - .add('Popup', () => ); + .add('Popup', () => ) + .add('MarkerLayer', () => ) + .add('ClusterMarkerLayer', () => ); diff --git a/stories/Components/components/clusterMarker.tsx b/stories/Components/components/clusterMarker.tsx new file mode 100644 index 0000000000..e49d7383bc --- /dev/null +++ b/stories/Components/components/clusterMarker.tsx @@ -0,0 +1,74 @@ +// @ts-ignore +import { Marker, MarkerLayer, PolygonLayer, Scene } from '@antv/l7'; +import { Mapbox, GaodeMap } from '@antv/l7-maps'; +import * as React from 'react'; +export default class ClusterMarkerLayer extends React.Component { + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public async componentDidMount() { + function getColor(v: number) { + return v > 50 + ? '#800026' + : v > 40 + ? '#BD0026' + : v > 30 + ? '#E31A1C' + : v > 20 + ? '#FC4E2A' + : v > 10 + ? '#FD8D3C' + : v > 5 + ? '#FEB24C' + : v > 0 + ? '#FED976' + : '#FFEDA0'; + } + + const response = await fetch( + 'https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json', + ); + const nodes = await response.json(); + const markerLayer = new MarkerLayer({ + cluster: true, + }); + const scene = new Scene({ + id: 'map', + map: new Mapbox({ + style: 'dark', + center: [110.19382669582967, 30.258134], + pitch: 0, + zoom: 3, + }), + }); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < nodes.features.length; i++) { + const { coordinates } = nodes.features[i].geometry; + const marker = new Marker().setLnglat({ + lng: coordinates[0], + lat: coordinates[1], + }); + markerLayer.addMarker(marker); + } + scene.addMarkerLayer(markerLayer); + this.scene = scene; + } + + public render() { + return ( +
+ ); + } +} diff --git a/stories/Components/components/markerlayer.tsx b/stories/Components/components/markerlayer.tsx new file mode 100644 index 0000000000..b51b29dbea --- /dev/null +++ b/stories/Components/components/markerlayer.tsx @@ -0,0 +1,78 @@ +// @ts-ignore +import { Marker, MarkerLayer, PolygonLayer, Scene } from '@antv/l7'; +import { GaodeMap } from '@antv/l7-maps'; +import * as React from 'react'; +export default class MarkerLayerComponent extends React.Component { + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public async componentDidMount() { + function getColor(v: number) { + return v > 50 + ? '#800026' + : v > 40 + ? '#BD0026' + : v > 30 + ? '#E31A1C' + : v > 20 + ? '#FC4E2A' + : v > 10 + ? '#FD8D3C' + : v > 5 + ? '#FEB24C' + : v > 0 + ? '#FED976' + : '#FFEDA0'; + } + + const response = await fetch( + 'https://gw.alipayobjects.com/os/basement_prod/67f47049-8787-45fc-acfe-e19924afe032.json', + ); + const nodes = await response.json(); + const markerLayer = new MarkerLayer(); + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [110.19382669582967, 30.258134], + pitch: 0, + zoom: 3, + }), + }); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].g !== '1' || nodes[i].v === '') { + continue; + } + const el = document.createElement('label'); + el.className = 'labelclass'; + el.textContent = nodes[i].v + '℃'; + el.style.background = getColor(nodes[i].v); + el.style.borderColor = getColor(nodes[i].v); + const marker = new Marker({ + element: el, + }).setLnglat({ lng: nodes[i].x * 1, lat: nodes[i].y }); + markerLayer.addMarker(marker); + } + scene.addMarkerLayer(markerLayer); + this.scene = scene; + } + + public render() { + return ( +
+ ); + } +}