1 | import * as React from 'react';
|
2 | import * as MapboxGL from 'mapbox-gl';
|
3 | const isEqual = require('deep-equal');
|
4 | import diff from './util/diff';
|
5 | import { Props as FeatureProps } from './feature';
|
6 |
|
7 | export type Paint =
|
8 | | MapboxGL.BackgroundPaint
|
9 | | MapboxGL.FillPaint
|
10 | | MapboxGL.FillExtrusionPaint
|
11 | | MapboxGL.SymbolPaint
|
12 | | MapboxGL.LinePaint
|
13 | | MapboxGL.RasterPaint
|
14 | | MapboxGL.CirclePaint;
|
15 |
|
16 | export type Layout =
|
17 | | MapboxGL.BackgroundLayout
|
18 | | MapboxGL.FillLayout
|
19 | | MapboxGL.FillExtrusionLayout
|
20 | | MapboxGL.LineLayout
|
21 | | MapboxGL.SymbolLayout
|
22 | | MapboxGL.RasterLayout
|
23 | | MapboxGL.CircleLayout;
|
24 |
|
25 | export interface ImageOptions {
|
26 | width?: number;
|
27 | height?: number;
|
28 | pixelRatio?: number;
|
29 | }
|
30 | export type ImageDefinition = [string, HTMLImageElement];
|
31 | export type ImageDefinitionWithOptions = [
|
32 | string,
|
33 | HTMLImageElement,
|
34 | ImageOptions
|
35 | ];
|
36 |
|
37 |
|
38 | export type MouseEvent = (evt: any) => any;
|
39 |
|
40 | export 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 |
|
52 | export 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 |
|
72 | metadata?: any;
|
73 | sourceLayer?: string;
|
74 | minZoom?: number;
|
75 | maxZoom?: number;
|
76 | geoJSONSourceOptions?: MapboxGL.GeoJSONSourceOptions;
|
77 |
|
78 | filter?: any[];
|
79 | children?: JSX.Element | JSX.Element[];
|
80 | }
|
81 |
|
82 | export interface OwnProps {
|
83 | id: string;
|
84 | draggedChildren?: JSX.Element[];
|
85 | map: MapboxGL.Map;
|
86 | }
|
87 |
|
88 | export type Props = LayerCommonProps & LayerEvents & OwnProps;
|
89 |
|
90 | type EventToHandlersType = {
|
91 | [key in keyof MapboxGL.MapLayerEventType]?: keyof LayerEvents
|
92 | };
|
93 |
|
94 | const 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 |
|
106 | export 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 |
|
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 |
|
188 |
|
189 | type: type as any,
|
190 | layout,
|
191 |
|
192 |
|
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 |
|
242 |
|
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 |
|
280 |
|
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 |
|
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 | }
|