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 { generateID } from './util/uid';
|
6 | import { Sources, LayerType } from './util/types';
|
7 | import { withMap } from './context';
|
8 |
|
9 | const types = ['symbol', 'line', 'fill', 'fill-extrusion', 'circle'];
|
10 | const 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 |
|
19 | const eventToHandler = {
|
20 | mousemove: 'OnMouseMove',
|
21 | mouseenter: 'OnMouseEnter',
|
22 | mouseleave: 'OnMouseLeave',
|
23 | mousedown: 'OnMouseDown',
|
24 | mouseup: 'OnMouseUp',
|
25 | click: 'OnClick'
|
26 | };
|
27 |
|
28 |
|
29 | export type MouseEvent = (evt: any) => any;
|
30 |
|
31 | export 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 |
|
42 | export 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 |
|
53 | export 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 |
|
64 | export 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 |
|
75 | export 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 |
|
86 | export 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 |
|
107 | type MapboxEventTypes = Array<keyof MapboxGL.MapLayerEventType>;
|
108 |
|
109 | type Paints =
|
110 | | MapboxGL.LinePaint
|
111 | | MapboxGL.SymbolPaint
|
112 | | MapboxGL.CirclePaint
|
113 | | MapboxGL.FillExtrusionPaint;
|
114 | type Layouts =
|
115 | | MapboxGL.FillLayout
|
116 | | MapboxGL.LineLayout
|
117 | | MapboxGL.CircleLayout
|
118 | | MapboxGL.FillExtrusionLayout;
|
119 |
|
120 | export class GeoJSONLayer extends React.Component<Props> {
|
121 | private id: string = this.props.id || `geojson-${generateID()}`;
|
122 |
|
123 |
|
124 | private source: Sources = {
|
125 | type: 'geojson',
|
126 | ...this.props.sourceOptions,
|
127 | data: this.props.data
|
128 |
|
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 |
|
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 |
|
155 |
|
156 | type: type as any,
|
157 |
|
158 |
|
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 |
|
188 |
|
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 |
|
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 |
|
342 | export default withMap(GeoJSONLayer);
|