UNPKG

9.5 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 { Props as FeatureProps } from './feature';
6
7export type Paint =
8 | MapboxGL.BackgroundPaint
9 | MapboxGL.FillPaint
10 | MapboxGL.FillExtrusionPaint
11 | MapboxGL.SymbolPaint
12 | MapboxGL.LinePaint
13 | MapboxGL.RasterPaint
14 | MapboxGL.CirclePaint;
15
16export type Layout =
17 | MapboxGL.BackgroundLayout
18 | MapboxGL.FillLayout
19 | MapboxGL.FillExtrusionLayout
20 | MapboxGL.LineLayout
21 | MapboxGL.SymbolLayout
22 | MapboxGL.RasterLayout
23 | MapboxGL.CircleLayout;
24
25export interface ImageOptions {
26 width?: number;
27 height?: number;
28 pixelRatio?: number;
29}
30export type ImageDefinition = [string, HTMLImageElement];
31export type ImageDefinitionWithOptions = [
32 string,
33 HTMLImageElement,
34 ImageOptions
35];
36
37// tslint:disable-next-line:no-any
38export type MouseEvent = (evt: any) => any;
39
40export interface LayerEvents {
41 onMouseMove?: MouseEvent;
42 onMouseEnter?: MouseEvent;
43 onMouseLeave?: MouseEvent;
44 onMouseDown?: MouseEvent;
45 onMouseUp?: MouseEvent;
46 onClick?: MouseEvent;
47 onTouchStart?: MouseEvent;
48 onTouchEnd?: MouseEvent;
49 onTouchCancel?: MouseEvent;
50}
51
52export interface LayerCommonProps {
53 type?:
54 | 'symbol'
55 | 'line'
56 | 'fill'
57 | 'circle'
58 | 'raster'
59 | 'fill-extrusion'
60 | 'background'
61 | 'heatmap';
62 sourceId?: string;
63 images?:
64 | ImageDefinition
65 | ImageDefinition[]
66 | ImageDefinitionWithOptions
67 | ImageDefinitionWithOptions[];
68 before?: string;
69 paint?: Paint;
70 layout?: Layout;
71 // tslint:disable-next-line:no-any
72 metadata?: any;
73 sourceLayer?: string;
74 minZoom?: number;
75 maxZoom?: number;
76 geoJSONSourceOptions?: MapboxGL.GeoJSONSourceOptions;
77 // tslint:disable-next-line:no-any
78 filter?: any[];
79 children?: JSX.Element | JSX.Element[];
80}
81
82export interface OwnProps {
83 id: string;
84 draggedChildren?: JSX.Element[];
85 map: MapboxGL.Map;
86}
87
88export type Props = LayerCommonProps & LayerEvents & OwnProps;
89
90type EventToHandlersType = {
91 [key in keyof MapboxGL.MapLayerEventType]?: keyof LayerEvents
92};
93
94const eventToHandler: EventToHandlersType = {
95 touchstart: 'onTouchStart',
96 touchend: 'onTouchEnd',
97 touchcancel: 'onTouchCancel',
98 mousemove: 'onMouseMove',
99 mouseenter: 'onMouseEnter',
100 mouseleave: 'onMouseLeave',
101 mousedown: 'onMouseDown',
102 mouseup: 'onMouseUp',
103 click: 'onClick'
104};
105
106export default class Layer extends React.Component<Props> {
107 public static defaultProps = {
108 type: 'symbol' as 'symbol',
109 layout: {},
110 paint: {}
111 };
112
113 private source: MapboxGL.GeoJSONSourceRaw = {
114 type: 'geojson',
115 ...this.props.geoJSONSourceOptions,
116 data: {
117 type: 'FeatureCollection',
118 features: []
119 }
120 };
121
122 // tslint:disable-next-line:no-any
123 private geometry = (coordinates: any): GeoJSON.Geometry => {
124 switch (this.props.type) {
125 case 'symbol':
126 case 'circle':
127 return {
128 type: 'Point',
129 coordinates
130 };
131
132 case 'fill':
133 if (Array.isArray(coordinates[0][0][0])) {
134 return {
135 type: 'MultiPolygon',
136 coordinates
137 };
138 }
139 return {
140 type: 'Polygon',
141 coordinates
142 };
143
144 case 'line':
145 return {
146 type: 'LineString',
147 coordinates
148 };
149
150 default:
151 return {
152 type: 'Point',
153 coordinates
154 };
155 }
156 };
157
158 private makeFeature = (
159 props: FeatureProps,
160 id: number
161 ): GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties> => ({
162 type: 'Feature',
163 geometry: this.geometry(props.coordinates),
164 properties: { ...props.properties, id }
165 });
166
167 private initialize = () => {
168 const {
169 type,
170 layout,
171 paint,
172 sourceId,
173 before,
174 images,
175 id,
176 metadata,
177 sourceLayer,
178 minZoom,
179 maxZoom,
180 filter
181 } = this.props;
182 const { map } = this.props;
183
184 const layer: MapboxGL.Layer = {
185 id,
186 source: sourceId || id,
187 // TODO: Fix mapbox-gl types
188 // tslint:disable-next-line:no-any
189 type: type as any,
190 layout,
191 // TODO: Fix mapbox-gl types
192 // tslint:disable-next-line:no-any
193 paint: paint as any,
194 metadata
195 };
196
197 if (sourceLayer) {
198 layer['source-layer'] = sourceLayer;
199 }
200
201 if (minZoom) {
202 layer.minzoom = minZoom;
203 }
204
205 if (maxZoom) {
206 layer.maxzoom = maxZoom;
207 }
208
209 if (filter) {
210 layer.filter = filter;
211 }
212
213 if (images) {
214 const normalizedImages = !Array.isArray(images[0]) ? [images] : images;
215 (normalizedImages as ImageDefinitionWithOptions[])
216 .filter(image => !map.hasImage(image[0]))
217 .forEach(image => {
218 map.addImage(image[0], image[1], image[2]);
219 });
220 }
221
222 if (!sourceId && !map.getSource(id)) {
223 map.addSource(id, this.source);
224 }
225
226 if (!map.getLayer(id)) {
227 map.addLayer(layer, before);
228 }
229
230 (Object.entries(eventToHandler) as Array<
231 [keyof EventToHandlersType, keyof LayerEvents]
232 >).forEach(([event, propName]) => {
233 const handler = this.props[propName];
234 if (handler) {
235 map.on(event, id, handler);
236 }
237 });
238 };
239
240 private onStyleDataChange = () => {
241 // if the style of the map has been updated and we don't have layer anymore,
242 // add it back to the map and force re-rendering to redraw it
243 if (!this.props.map.getLayer(this.props.id)) {
244 this.initialize();
245 this.forceUpdate();
246 }
247 };
248
249 public componentDidMount() {
250 const { map } = this.props;
251
252 this.initialize();
253
254 map.on('styledata', this.onStyleDataChange);
255 }
256
257 public componentWillUnmount() {
258 const { map } = this.props;
259 const { images, id } = this.props;
260
261 if (!map || !map.getStyle()) {
262 return;
263 }
264
265 map.off('styledata', this.onStyleDataChange);
266
267 (Object.entries(eventToHandler) as Array<
268 [keyof EventToHandlersType, keyof LayerEvents]
269 >).forEach(([event, propName]) => {
270 const handler = this.props[propName];
271 if (handler) {
272 map.off(event, id, handler);
273 }
274 });
275
276 if (map.getLayer(id)) {
277 map.removeLayer(id);
278 }
279 // if pointing to an existing source, don't remove
280 // as other layers may be dependent upon it
281 if (!this.props.sourceId) {
282 map.removeSource(id);
283 }
284
285 if (images) {
286 const normalizedImages = !Array.isArray(images[0]) ? [images] : images;
287 (normalizedImages as ImageDefinitionWithOptions[])
288 .map(([key, ...rest]) => key)
289 .forEach(map.removeImage.bind(map));
290 }
291 }
292
293 public componentDidUpdate(prevProps: Props) {
294 const {
295 paint,
296 layout,
297 before,
298 filter,
299 id,
300 minZoom,
301 maxZoom,
302 map
303 } = prevProps;
304
305 if (!isEqual(this.props.paint, paint)) {
306 const paintDiff = diff(paint, this.props.paint);
307
308 Object.keys(paintDiff).forEach(key => {
309 map.setPaintProperty(id, key, paintDiff[key]);
310 });
311 }
312
313 if (!isEqual(this.props.layout, layout)) {
314 const layoutDiff = diff(layout, this.props.layout);
315
316 Object.keys(layoutDiff).forEach(key => {
317 map.setLayoutProperty(id, key, layoutDiff[key]);
318 });
319 }
320
321 if (!isEqual(this.props.filter, filter)) {
322 map.setFilter(id, this.props.filter);
323 }
324
325 if (before !== this.props.before) {
326 map.moveLayer(id, this.props.before);
327 }
328
329 if (minZoom !== this.props.minZoom || maxZoom !== this.props.maxZoom) {
330 // TODO: Fix when PR https://github.com/DefinitelyTyped/DefinitelyTyped/pull/22036 is merged
331 map.setLayerZoomRange(id, this.props.minZoom!, this.props.maxZoom!);
332 }
333
334 (Object.entries(eventToHandler) as Array<
335 [keyof EventToHandlersType, keyof LayerEvents]
336 >).forEach(([event, propName]) => {
337 const oldHandler = prevProps[propName];
338 const newHandler = this.props[propName];
339
340 if (oldHandler !== newHandler) {
341 if (oldHandler) {
342 map.off(event, id, oldHandler);
343 }
344
345 if (newHandler) {
346 map.on(event, id, newHandler);
347 }
348 }
349 });
350 }
351
352 public getChildren = () => {
353 const { children } = this.props;
354
355 if (!children) {
356 return [];
357 }
358
359 if (Array.isArray(children)) {
360 return (children as JSX.Element[][]).reduce(
361 (arr, next) => arr.concat(next),
362 [] as JSX.Element[]
363 );
364 }
365
366 return [children] as JSX.Element[];
367 };
368
369 public render() {
370 const { map } = this.props;
371 const { sourceId, draggedChildren } = this.props;
372 let children = this.getChildren();
373
374 if (draggedChildren) {
375 const draggableChildrenIds = draggedChildren.map(child => child.key);
376 children = children.map(child => {
377 const indexChildren = draggableChildrenIds.indexOf(child.key);
378 if (indexChildren !== -1) {
379 return draggedChildren[indexChildren];
380 }
381 return child;
382 });
383 }
384
385 const features = (children! as Array<React.ReactElement<FeatureProps>>)
386 .map(({ props }, id) => this.makeFeature(props, id))
387 .filter(Boolean);
388
389 const source = map.getSource(
390 sourceId || this.props.id
391 ) as MapboxGL.GeoJSONSource;
392
393 if (source && !sourceId && source.setData) {
394 source.setData({
395 type: 'FeatureCollection',
396 features: features as GeoJSON.Feature[]
397 });
398 }
399
400 return null;
401 }
402}