UNPKG

6.21 kBTypeScriptView Raw
1import * as React from 'react';
2import { LngLatBounds, Map } from 'mapbox-gl';
3import { Props as MarkerProps } from './marker';
4import Supercluster from 'supercluster';
5import * as GeoJSON from 'geojson';
6import bbox from '@turf/bbox';
7import { polygon, featureCollection } from '@turf/helpers';
8import { withMap } from './context';
9
10export interface Props {
11 ClusterMarkerFactory(
12 coordinates: GeoJSON.Position,
13 pointCount: number,
14 getLeaves: (
15 limit?: number,
16 offset?: number
17 ) => Array<React.ReactElement<MarkerProps> | undefined>
18 ): React.ReactElement<MarkerProps>;
19 radius?: number;
20 maxZoom?: number;
21 minZoom?: number;
22 extent?: number;
23 nodeSize?: number;
24 log?: boolean;
25 zoomOnClick?: boolean;
26 zoomOnClickPadding?: number;
27 children?: Array<React.ReactElement<MarkerProps>>;
28 style?: React.CSSProperties;
29 className?: string;
30 tabIndex?: number;
31 map: Map;
32}
33
34export interface State {
35 superC: Supercluster;
36 clusterPoints: Array<GeoJSON.Feature<GeoJSON.Point>>;
37}
38
39export class Cluster extends React.Component<Props, State> {
40 public static defaultProps = {
41 radius: 60,
42 minZoom: 0,
43 maxZoom: 16,
44 extent: 512,
45 nodeSize: 64,
46 log: false,
47 zoomOnClick: false,
48 zoomOnClickPadding: 20
49 };
50
51 public state: State = {
52 superC: new Supercluster({
53 radius: this.props.radius,
54 maxZoom: this.props.maxZoom,
55 minZoom: this.props.minZoom,
56 extent: this.props.extent,
57 nodeSize: this.props.nodeSize,
58 log: this.props.log
59 }),
60 clusterPoints: []
61 };
62
63 private featureClusterMap = new WeakMap<
64 GeoJSON.Feature,
65 React.ReactElement<MarkerProps>
66 >();
67
68 public componentDidMount() {
69 const { children, map } = this.props;
70
71 if (children) {
72 this.childrenChange(children as Array<React.ReactElement<MarkerProps>>);
73 }
74
75 map.on('move', this.mapChange);
76 map.on('zoom', this.mapChange);
77 this.mapChange();
78 }
79
80 public componentWillUnmount() {
81 const { map } = this.props;
82
83 map.off('move', this.mapChange);
84 map.off('zoom', this.mapChange);
85 }
86
87 public componentDidUpdate(prevProps: Props) {
88 const { children } = prevProps;
89
90 if (children !== this.props.children && this.props.children) {
91 this.childrenChange(this.props.children);
92 this.mapChange(true);
93 }
94 }
95
96 private childrenChange = (
97 newChildren: Array<React.ReactElement<MarkerProps>>
98 ) => {
99 const { superC } = this.state;
100 this.featureClusterMap = new WeakMap<
101 GeoJSON.Feature,
102 React.ReactElement<MarkerProps>
103 >();
104 const features = this.childrenToFeatures(newChildren);
105 superC.load(features);
106 };
107
108 private mapChange = (forceSetState: boolean = false) => {
109 const { map } = this.props;
110 const { superC, clusterPoints } = this.state;
111
112 const zoom = map.getZoom();
113 const canvas = map.getCanvas();
114 const w = canvas.width;
115 const h = canvas.height;
116 const upLeft = map.unproject([0, 0]).toArray();
117 const upRight = map.unproject([w, 0]).toArray();
118 const downRight = map.unproject([w, h]).toArray();
119 const downLeft = map.unproject([0, h]).toArray();
120 const newPoints = superC.getClusters(
121 bbox(polygon([[upLeft, upRight, downRight, downLeft, upLeft]])),
122 Math.round(zoom)
123 );
124 if (newPoints.length !== clusterPoints.length || forceSetState) {
125 this.setState({ clusterPoints: newPoints });
126 }
127 };
128
129 private feature(coordinates: GeoJSON.Position) {
130 return {
131 type: 'Feature' as 'Feature',
132 geometry: {
133 type: 'Point' as 'Point',
134 coordinates
135 },
136 properties: {}
137 };
138 }
139
140 private childrenToFeatures = (
141 children: Array<React.ReactElement<MarkerProps>>
142 ) =>
143 children.map(child => {
144 const feature = this.feature(child && child.props.coordinates);
145 this.featureClusterMap.set(feature, child);
146 return feature;
147 });
148
149 private getLeaves = (
150 feature: GeoJSON.Feature,
151 limit?: number,
152 offset?: number
153 ) => {
154 const { superC } = this.state;
155 return superC
156 .getLeaves(
157 feature.properties && feature.properties.cluster_id,
158 limit || Infinity,
159 offset
160 )
161 .map((leave: GeoJSON.Feature) => this.featureClusterMap.get(leave));
162 };
163
164 public zoomToClusterBounds = (event: React.MouseEvent<HTMLElement>) => {
165 const markers = Array.prototype.slice.call(event.currentTarget.children);
166 const marker = this.findMarkerElement(
167 event.currentTarget,
168 event.target as HTMLElement
169 );
170 const index = markers.indexOf(marker);
171 const cluster = this.state.clusterPoints[index] as GeoJSON.Feature;
172 if (!cluster.properties || !cluster.properties.cluster_id) {
173 return;
174 }
175 const children = this.state.superC.getLeaves(
176 cluster.properties && cluster.properties.cluster_id,
177 Infinity
178 );
179 const childrenBbox = bbox(featureCollection(children));
180 // https://github.com/mapbox/mapbox-gl-js/issues/5249
181 // tslint:disable-next-line:no-any
182 this.props.map.fitBounds(LngLatBounds.convert(childrenBbox as any), {
183 padding: this.props.zoomOnClickPadding!
184 });
185 };
186
187 private findMarkerElement(
188 target: HTMLElement,
189 element: HTMLElement
190 ): HTMLElement {
191 if (element.parentElement === target) {
192 return element;
193 }
194 return this.findMarkerElement(target, element.parentElement!);
195 }
196
197 public render() {
198 const { ClusterMarkerFactory, style, className, tabIndex } = this.props;
199 const { clusterPoints } = this.state;
200
201 return (
202 <div
203 style={style}
204 className={className}
205 tabIndex={tabIndex}
206 onClick={this.props.zoomOnClick ? this.zoomToClusterBounds : undefined}
207 >
208 {clusterPoints.map((feature: GeoJSON.Feature<GeoJSON.Point>) => {
209 if (feature.properties && feature.properties.cluster) {
210 return ClusterMarkerFactory(
211 feature.geometry.coordinates,
212 feature.properties.point_count,
213 this.getLeaves.bind(this, feature)
214 );
215 }
216 return this.featureClusterMap.get(feature);
217 })}
218 </div>
219 );
220 }
221}
222
223export default withMap(Cluster);