UNPKG

8.98 kBPlain TextView Raw
1import * as React from 'react';
2import * as MapboxGL from 'mapbox-gl';
3const isEqual = require('deep-equal'); //tslint:disable-line
4import diff from './util/diff';
5import { generateID } from './util/uid';
6import { Sources, LayerType } from './util/types';
7import { withMap } from './context';
8
9const types = ['symbol', 'line', 'fill', 'fill-extrusion', 'circle'];
10const toCamelCase = (str: string) =>
11 str
12 .replace(
13 /(?:^\w|[A-Z]|\b\w)/g,
14 (letter, index) =>
15 index === 0 ? letter.toLowerCase() : letter.toUpperCase()
16 )
17 .replace(/[\s+]|-/g, '');
18
19const eventToHandler = {
20 mousemove: 'OnMouseMove',
21 mouseenter: 'OnMouseEnter',
22 mouseleave: 'OnMouseLeave',
23 mousedown: 'OnMouseDown',
24 mouseup: 'OnMouseUp',
25 click: 'OnClick'
26};
27
28// tslint:disable-next-line:no-any
29export type MouseEvent = (evt: any) => any;
30
31export interface LineProps {
32 linePaint?: MapboxGL.LinePaint;
33 lineLayout?: MapboxGL.LineLayout;
34 lineOnMouseMove?: MouseEvent;
35 lineOnMouseEnter?: MouseEvent;
36 lineOnMouseLeave?: MouseEvent;
37 lineOnMouseDown?: MouseEvent;
38 lineOnMouseUp?: MouseEvent;
39 lineOnClick?: MouseEvent;
40}
41
42export interface CircleProps {
43 circlePaint?: MapboxGL.CirclePaint;
44 circleLayout?: MapboxGL.CircleLayout;
45 circleOnMouseMove?: MouseEvent;
46 circleOnMouseEnter?: MouseEvent;
47 circleOnMouseLeave?: MouseEvent;
48 circleOnMouseDown?: MouseEvent;
49 circleOnMouseUp?: MouseEvent;
50 circleOnClick?: MouseEvent;
51}
52
53export interface SymbolProps {
54 symbolLayout?: MapboxGL.SymbolLayout;
55 symbolPaint?: MapboxGL.SymbolPaint;
56 symbolOnMouseMove?: MouseEvent;
57 symbolOnMouseEnter?: MouseEvent;
58 symbolOnMouseLeave?: MouseEvent;
59 symbolOnMouseDown?: MouseEvent;
60 symbolOnMouseUp?: MouseEvent;
61 symbolOnClick?: MouseEvent;
62}
63
64export interface FillProps {
65 fillLayout?: MapboxGL.FillLayout;
66 fillPaint?: MapboxGL.FillPaint;
67 fillOnMouseMove?: MouseEvent;
68 fillOnMouseEnter?: MouseEvent;
69 fillOnMouseLeave?: MouseEvent;
70 fillOnMouseDown?: MouseEvent;
71 fillOnMouseUp?: MouseEvent;
72 fillOnClick?: MouseEvent;
73}
74
75export interface FillExtrusionProps {
76 fillExtrusionLayout?: MapboxGL.FillExtrusionLayout;
77 fillExtrusionPaint?: MapboxGL.FillExtrusionPaint;
78 fillExtrusionOnMouseMove?: MouseEvent;
79 fillExtrusionOnMouseEnter?: MouseEvent;
80 fillExtrusionOnMouseLeave?: MouseEvent;
81 fillExtrusionOnMouseDown?: MouseEvent;
82 fillExtrusionOnMouseUp?: MouseEvent;
83 fillExtrusionOnClick?: MouseEvent;
84}
85
86export interface Props
87 extends LineProps,
88 CircleProps,
89 SymbolProps,
90 FillProps,
91 FillExtrusionProps {
92 id?: string;
93 data:
94 | GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>
95 | GeoJSON.FeatureCollection<GeoJSON.Geometry>
96 | string;
97 layerOptions?: MapboxGL.Layer;
98 sourceOptions?:
99 | MapboxGL.VectorSource
100 | MapboxGL.RasterSource
101 | MapboxGL.GeoJSONSource
102 | MapboxGL.GeoJSONSourceRaw;
103 before?: string;
104 map: MapboxGL.Map;
105}
106
107type MapboxEventTypes = Array<keyof MapboxGL.MapLayerEventType>;
108
109type Paints =
110 | MapboxGL.LinePaint
111 | MapboxGL.SymbolPaint
112 | MapboxGL.CirclePaint
113 | MapboxGL.FillExtrusionPaint;
114type Layouts =
115 | MapboxGL.FillLayout
116 | MapboxGL.LineLayout
117 | MapboxGL.CircleLayout
118 | MapboxGL.FillExtrusionLayout;
119
120export class GeoJSONLayer extends React.Component<Props> {
121 private id: string = this.props.id || `geojson-${generateID()}`;
122
123 // TODO: Refactor to use defaultProps
124 private source: Sources = {
125 type: 'geojson',
126 ...this.props.sourceOptions,
127 data: this.props.data
128 // tslint:disable-next-line:no-any
129 } as any;
130
131 private layerIds: string[] = [];
132
133 private buildLayerId = (type: string) => {
134 return `${this.id}-${type}`;
135 };
136
137 private createLayer = (type: LayerType) => {
138 const { before, layerOptions, map } = this.props;
139
140 const layerId = this.buildLayerId(type);
141 this.layerIds.push(layerId);
142
143 const paint: Paints = this.props[`${toCamelCase(type)}Paint`] || {};
144
145 // default undefined layers to invisible
146 const visibility = Object.keys(paint).length ? 'visible' : 'none';
147 const layout: Layouts = this.props[`${toCamelCase(type)}Layout`] || {
148 visibility
149 };
150
151 const layer: MapboxGL.Layer = {
152 id: layerId,
153 source: this.id,
154 // TODO: Fix mapbox-gl types
155 // tslint:disable-next-line:no-any
156 type: type as any,
157 // TODO: Fix mapbox-gl types
158 // tslint:disable-next-line:no-any
159 paint: paint as any,
160 layout,
161 ...layerOptions
162 };
163
164 map.addLayer(layer, before);
165
166 this.mapLayerMouseHandlers(type);
167 };
168
169 private mapLayerMouseHandlers = (type: string) => {
170 const { map } = this.props;
171
172 const layerId = this.buildLayerId(type);
173
174 const events = Object.keys(eventToHandler) as MapboxEventTypes;
175
176 events.forEach(event => {
177 const handler =
178 this.props[`${toCamelCase(type)}${eventToHandler[event]}`] || null;
179
180 if (handler) {
181 map.on(event, layerId, handler);
182 }
183 });
184 };
185
186 private onStyleDataChange = () => {
187 // if the style of the map has been updated and we don't have layer anymore,
188 // add it back to the map and force re-rendering to redraw it
189 if (!this.props.map.getSource(this.id)) {
190 this.unbind();
191 this.initialize();
192 this.forceUpdate();
193 }
194 };
195
196 private initialize() {
197 const { map } = this.props;
198
199 map.addSource(this.id, this.source);
200
201 this.createLayer('symbol');
202 this.createLayer('line');
203 this.createLayer('fill');
204 this.createLayer('fill-extrusion');
205 this.createLayer('circle');
206 }
207
208 private unbind() {
209 const { map } = this.props;
210
211 if (map.getSource(this.id)) {
212 const { layers } = map.getStyle();
213
214 if (layers) {
215 layers
216 .filter(layer => layer.source === this.id)
217 .forEach(layer => map.removeLayer(layer.id));
218 }
219
220 map.removeSource(this.id);
221 }
222
223 types.forEach(type => {
224 const events = Object.keys(eventToHandler) as MapboxEventTypes;
225 events.forEach(event => {
226 const prop = toCamelCase(type) + eventToHandler[event];
227
228 if (this.props[prop]) {
229 map.off(event, this.buildLayerId(type), this.props[prop]);
230 }
231 });
232 });
233
234 this.layerIds.forEach(lId => {
235 if (map.getLayer(lId)) {
236 map.removeLayer(lId);
237 }
238 });
239 }
240
241 public componentDidMount() {
242 const { map } = this.props;
243 this.initialize();
244 map.on('styledata', this.onStyleDataChange);
245 }
246
247 public componentWillUnmount() {
248 const { map } = this.props;
249
250 if (!map || !map.getStyle()) {
251 return;
252 }
253
254 map.off('styledata', this.onStyleDataChange);
255
256 this.unbind();
257 }
258
259 public isGeoJSONSource = (
260 source?: Sources
261 ): source is MapboxGL.GeoJSONSource =>
262 !!source &&
263 typeof (source as MapboxGL.GeoJSONSource).setData === 'function';
264
265 public componentDidUpdate(prevProps: Props) {
266 const { data, before, layerOptions, map } = prevProps;
267 const source = map.getSource(this.id);
268 if (!this.isGeoJSONSource(source)) {
269 return;
270 }
271
272 if (this.props.data !== data) {
273 source.setData(this.props.data);
274
275 this.source = {
276 type: 'geojson',
277 ...this.props.sourceOptions,
278 data: this.props.data
279 // tslint:disable-next-line:no-any
280 } as any;
281 }
282
283 const layerFilterChanged =
284 this.props.layerOptions &&
285 layerOptions &&
286 !isEqual(this.props.layerOptions.filter, layerOptions.filter);
287
288 types.forEach(type => {
289 const layerId = this.buildLayerId(type);
290
291 if (this.props.layerOptions && layerFilterChanged) {
292 map.setFilter(layerId, this.props.layerOptions.filter || []);
293 }
294
295 const paintProp = toCamelCase(type) + 'Paint';
296
297 if (!isEqual(prevProps[paintProp], this.props[paintProp])) {
298 const paintDiff = diff(prevProps[paintProp], this.props[paintProp]);
299
300 Object.keys(paintDiff).forEach(key => {
301 map.setPaintProperty(layerId, key, paintDiff[key]);
302 });
303 }
304
305 const layoutProp = toCamelCase(type) + 'Layout';
306
307 if (!isEqual(prevProps[layoutProp], this.props[layoutProp])) {
308 const layoutDiff = diff(prevProps[layoutProp], this.props[layoutProp]);
309
310 Object.keys(layoutDiff).forEach(key => {
311 map.setLayoutProperty(layerId, key, layoutDiff[key]);
312 });
313 }
314
315 const events = Object.keys(eventToHandler) as MapboxEventTypes;
316
317 events.forEach(event => {
318 const prop = toCamelCase(type) + eventToHandler[event];
319
320 if (prevProps[prop] !== this.props[prop]) {
321 if (prevProps[prop]) {
322 map.off(event, layerId, prevProps[prop]);
323 }
324
325 if (this.props[prop]) {
326 map.on(event, layerId, this.props[prop]);
327 }
328 }
329 });
330
331 if (before !== this.props.before) {
332 map.moveLayer(layerId, this.props.before);
333 }
334 });
335 }
336
337 public render() {
338 return null;
339 }
340}
341
342export default withMap(GeoJSONLayer);