mirror of https://gitee.com/antv-l7/antv-l7
feat(component): 新增cluster marker
This commit is contained in:
parent
cf91b111ff
commit
41624f12fe
|
@ -8,7 +8,6 @@ L7 能够满足常见的地图图表,BI 系统的可视化分析、以及 GIS
|
|||
|
||||
![l7 demo](https://gw.alipayobjects.com/mdn/rms_855bab/afts/img/A*S-73QpO8d0YAAAAAAAAAAABkARQnAQ)
|
||||
|
||||
|
||||
## 🌟 核心特性
|
||||
|
||||
🌏 数据驱动可视化展示
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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<unknown>;
|
||||
private scene: Container;
|
||||
private zoom: number;
|
||||
|
||||
constructor(option?: Partial<IMarkerLayerOption>) {
|
||||
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<IMapService>(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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.lableclass {
|
||||
.labelclass {
|
||||
position: absolute;
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -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', () => <Scale />)
|
||||
.add('Marker', () => <Marker />)
|
||||
.add('Chart', () => <Chart />)
|
||||
.add('Popup', () => <Popup />);
|
||||
.add('Popup', () => <Popup />)
|
||||
.add('MarkerLayer', () => <MarkerLayerComponent />)
|
||||
.add('ClusterMarkerLayer', () => <ClusterMarkerLayer />);
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
id="map"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div
|
||||
id="map"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue