UNPKG

13.7 kBJavaScriptView Raw
1// @flow
2import { _MapContext as MapContext } from 'react-map-gl';
3import React, { PureComponent } from 'react';
4import { ImmutableFeatureCollection } from '@nebula.gl/edit-modes';
5
6import type { Feature, Position, EditAction } from '@nebula.gl/edit-modes';
7import type { MjolnirEvent } from 'mjolnir.js';
8import type { BaseEvent, EditorProps, EditorState, SelectAction } from './types';
9import memoize from './memoize';
10
11import { DRAWING_MODE, EDIT_TYPE, ELEMENT_TYPE, MODES } from './constants';
12import { getScreenCoords, isNumeric, parseEventElement } from './edit-modes/utils';
13import {
14 SelectMode,
15 EditingMode,
16 DrawPointMode,
17 DrawLineStringMode,
18 DrawRectangleMode,
19 DrawPolygonMode
20} from './edit-modes';
21
22const MODE_TO_HANDLER = Object.freeze({
23 [MODES.READ_ONLY]: null,
24 [MODES.SELECT]: SelectMode,
25 [MODES.EDITING]: EditingMode,
26 [MODES.DRAW_POINT]: DrawPointMode,
27 [MODES.DRAW_PATH]: DrawLineStringMode,
28 [MODES.DRAW_RECTANGLE]: DrawRectangleMode,
29 [MODES.DRAW_POLYGON]: DrawPolygonMode
30});
31
32const defaultProps = {
33 mode: MODES.READ_ONLY,
34 features: null,
35 onSelect: null,
36 onUpdate: null
37};
38
39const defaultState = {
40 featureCollection: new ImmutableFeatureCollection({
41 type: 'FeatureCollection',
42 features: []
43 }),
44
45 selectedFeatureIndex: null,
46
47 // index, isGuide, mapCoords, screenCoords
48 hovered: null,
49
50 isDragging: false,
51 didDrag: false,
52
53 lastPointerMoveEvent: null,
54
55 pointerDownPicks: null,
56 pointerDownScreenCoords: null,
57 pointerDownMapCoords: null
58};
59
60export default class ModeHandler extends PureComponent<EditorProps, EditorState> {
61 static defaultProps = defaultProps;
62
63 constructor() {
64 super();
65 this.state = defaultState;
66 this._eventsRegistered = false;
67
68 this._events = {
69 anyclick: evt => this._onEvent(this._onClick, evt, true),
70 click: evt => evt.stopImmediatePropagation(),
71 pointermove: evt => this._onEvent(this._onPointerMove, evt, true),
72 pointerdown: evt => this._onEvent(this._onPointerDown, evt, true),
73 pointerup: evt => this._onEvent(this._onPointerUp, evt, true),
74 panmove: evt => this._onEvent(this._onPan, evt, false),
75 panstart: evt => this._onEvent(this._onPan, evt, false),
76 panend: evt => this._onEvent(this._onPan, evt, false)
77 };
78 }
79
80 componentDidMount() {
81 this._setupModeHandler();
82 }
83
84 componentDidUpdate(prevProps: EditorProps) {
85 if (prevProps.mode !== this.props.mode) {
86 this._clearEditingState();
87 this._setupModeHandler();
88 }
89 }
90
91 componentWillUnmount() {
92 this._degregisterEvents();
93 }
94
95 _events: any;
96 _eventsRegistered: boolean;
97 _modeHandler: any;
98 _context: ?MapContext;
99 _containerRef: ?HTMLElement;
100
101 getFeatures = () => {
102 let featureCollection = this._getFeatureCollection();
103 featureCollection = featureCollection && featureCollection.getObject();
104 return featureCollection && featureCollection.features;
105 };
106
107 addFeatures = (features: Feature | Feature[]) => {
108 let featureCollection = this._getFeatureCollection();
109 if (featureCollection) {
110 if (!Array.isArray(features)) {
111 features = [features];
112 }
113
114 featureCollection = featureCollection.addFeatures(features);
115 this.setState({ featureCollection });
116 }
117 };
118
119 deleteFeatures = (featureIndexes: number | number[]) => {
120 let featureCollection = this._getFeatureCollection();
121 const selectedFeatureIndex = this._getSelectedFeatureIndex();
122 if (featureCollection) {
123 if (!Array.isArray(featureIndexes)) {
124 featureIndexes = [featureIndexes];
125 }
126 featureCollection = featureCollection.deleteFeatures(featureIndexes);
127 const newState: any = { featureCollection };
128 if (featureIndexes.findIndex(index => selectedFeatureIndex === index) >= 0) {
129 newState.selectedFeatureIndex = null;
130 }
131 this.setState(newState);
132 }
133 };
134
135 getModeProps() {
136 const featureCollection = this._getFeatureCollection();
137
138 const { lastPointerMoveEvent } = this.state;
139 const selectedFeatureIndex = this._getSelectedFeatureIndex();
140 const viewport = this._context && this._context.viewport;
141
142 return {
143 data: featureCollection,
144 selectedIndexes: [selectedFeatureIndex],
145 lastPointerMoveEvent,
146 viewport,
147 onEdit: this._onEdit
148 };
149 }
150
151 /* MEMORIZERS */
152 _getMemorizedFeatureCollection = memoize(({ propsFeatures, stateFeatures }: any) => {
153 const features = propsFeatures || stateFeatures;
154 // Any changes in ImmutableFeatureCollection will create a new object
155 if (features instanceof ImmutableFeatureCollection) {
156 return features;
157 }
158
159 if (features && features.type === 'FeatureCollection') {
160 return new ImmutableFeatureCollection({
161 type: 'FeatureCollection',
162 features: features.features
163 });
164 }
165
166 return new ImmutableFeatureCollection({
167 type: 'FeatureCollection',
168 features: features || []
169 });
170 });
171
172 _getFeatureCollection = () => {
173 return this._getMemorizedFeatureCollection({
174 propsFeatures: this.props.features,
175 stateFeatures: this.state.featureCollection
176 });
177 };
178
179 _setupModeHandler = () => {
180 const mode = this.props.mode;
181
182 if (!mode || mode === MODES.READ_ONLY) {
183 this._degregisterEvents();
184 this._modeHandler = null;
185 return;
186 }
187
188 this._registerEvents();
189
190 const HandlerClass = MODE_TO_HANDLER[mode];
191 this._modeHandler = HandlerClass ? new HandlerClass() : null;
192 };
193
194 /* EDITING OPERATIONS */
195 _clearEditingState = () => {
196 this.setState({
197 selectedFeatureIndex: null,
198
199 hovered: null,
200
201 pointerDownPicks: null,
202 pointerDownScreenCoords: null,
203 pointerDownMapCoords: null,
204
205 isDragging: false,
206 didDrag: false
207 });
208 };
209
210 _getSelectedFeatureIndex = () => {
211 if ('selectedFeatureIndex' in this.props) {
212 return this.props.selectedFeatureIndex;
213 }
214 return this.state.selectedFeatureIndex;
215 };
216
217 _getSelectedFeature = (featureIndex: ?number) => {
218 const features = this.getFeatures();
219 featureIndex = isNumeric(featureIndex) ? featureIndex : this._getSelectedFeatureIndex();
220 return features[featureIndex];
221 };
222
223 _onSelect = (selected: SelectAction) => {
224 this.setState({ selectedFeatureIndex: selected && selected.selectedFeatureIndex });
225 if (this.props.onSelect) {
226 this.props.onSelect(selected);
227 }
228 };
229
230 _onUpdate = (editAction: EditAction, isInternal: ?boolean) => {
231 const { editType, updatedData, editContext } = editAction;
232 this.setState({ featureCollection: new ImmutableFeatureCollection(updatedData) });
233 if (this.props.onUpdate && !isInternal) {
234 this.props.onUpdate({
235 data: updatedData && updatedData.features,
236 editType,
237 editContext
238 });
239 }
240 };
241
242 _onEdit = (editAction: EditAction) => {
243 const { mode } = this.props;
244 const { editType, updatedData } = editAction;
245
246 switch (editType) {
247 case EDIT_TYPE.MOVE_POSITION:
248 // intermediate feature, do not need forward to application
249 // only need update editor internal state
250 this._onUpdate(editAction, !Boolean(this.props.features));
251 break;
252 case EDIT_TYPE.ADD_FEATURE:
253 this._onUpdate(editAction);
254 if (mode === MODES.DRAW_PATH) {
255 const context = (editAction.editContext && editAction.editContext[0]) || {};
256 const { screenCoords, mapCoords } = context;
257 const featureIndex = updatedData.features.length - 1;
258 const selectedFeature = this._getSelectedFeature(featureIndex);
259 this._onSelect({
260 selectedFeature,
261 selectedFeatureIndex: featureIndex,
262 selectedEditHandleIndex: null,
263 screenCoords,
264 mapCoords
265 });
266 }
267 break;
268 case EDIT_TYPE.ADD_POSITION:
269 case EDIT_TYPE.REMOVE_POSITION:
270 case EDIT_TYPE.FINISH_MOVE_POSITION:
271 this._onUpdate(editAction);
272 break;
273
274 default:
275 }
276 };
277
278 /* EVENTS */
279 _degregisterEvents = () => {
280 const eventManager = this._context && this._context.eventManager;
281 if (!this._events || !eventManager) {
282 return;
283 }
284
285 if (this._eventsRegistered) {
286 eventManager.off(this._events);
287 this._eventsRegistered = false;
288 }
289 };
290
291 _registerEvents = () => {
292 const ref = this._containerRef;
293 const eventManager = this._context && this._context.eventManager;
294 if (!this._events || !ref || !eventManager) {
295 return;
296 }
297
298 if (this._eventsRegistered) {
299 return;
300 }
301
302 eventManager.on(this._events, ref);
303 this._eventsRegistered = true;
304 };
305
306 _onEvent = (handler: Function, evt: MjolnirEvent, stopPropagation: boolean) => {
307 const event = this._getEvent(evt);
308 handler(event);
309
310 if (stopPropagation) {
311 evt.stopImmediatePropagation();
312 }
313 };
314
315 _onClick = (event: BaseEvent) => {
316 const { mode } = this.props;
317 if (mode === MODES.SELECT || mode === MODES.EDITING) {
318 const { mapCoords, screenCoords } = event;
319 const pickedObject = event.picks && event.picks[0] && event.picks[0].object;
320 if (pickedObject && isNumeric(pickedObject.featureIndex)) {
321 const selectedFeatureIndex = pickedObject.featureIndex;
322 const selectedFeature = this._getSelectedFeature(selectedFeatureIndex);
323 this._onSelect({
324 selectedFeature,
325 selectedFeatureIndex,
326 selectedEditHandleIndex:
327 pickedObject.type === ELEMENT_TYPE.EDIT_HANDLE ? pickedObject.index : null,
328 mapCoords,
329 screenCoords
330 });
331 } else {
332 this._onSelect({
333 selectedFeature: null,
334 selectedFeatureIndex: null,
335 selectedEditHandleIndex: null,
336 mapCoords,
337 screenCoords
338 });
339 }
340 }
341
342 const modeProps = this.getModeProps();
343 if(this._modeHandler) this._modeHandler.handleClick(event, modeProps);
344 };
345
346 _onPointerMove = (event: BaseEvent) => {
347 // hovering
348 const hovered = this._getHoverState(event);
349 const {
350 isDragging,
351 didDrag,
352 pointerDownPicks,
353 pointerDownScreenCoords,
354 pointerDownMapCoords
355 } = this.state;
356
357 if (isDragging && !didDrag && pointerDownScreenCoords) {
358 const dx = event.screenCoords[0] - pointerDownScreenCoords[0];
359 const dy = event.screenCoords[1] - pointerDownScreenCoords[1];
360 if (dx * dx + dy * dy > 5) {
361 this.setState({ didDrag: true });
362 }
363 }
364
365 const pointerMoveEvent = {
366 ...event,
367 isDragging,
368 pointerDownPicks,
369 pointerDownScreenCoords,
370 pointerDownMapCoords
371 };
372
373 if (this.state.didDrag) {
374 const modeProps = this.getModeProps();
375 this._modeHandler.handlePointerMove(pointerMoveEvent, modeProps);
376 }
377
378 this.setState({
379 hovered,
380 lastPointerMoveEvent: pointerMoveEvent
381 });
382 };
383
384 _onPointerDown = (event: BaseEvent) => {
385 const pickedObject = event.picks && event.picks[0] && event.picks[0].object;
386 const startDraggingEvent = {
387 ...event,
388 pointerDownScreenCoords: event.screenCoords,
389 pointerDownMapCoords: event.mapCoords
390 };
391
392 const newState = {
393 isDragging: pickedObject && isNumeric(pickedObject.featureIndex),
394 pointerDownPicks: event.picks,
395 pointerDownScreenCoords: event.screenCoords,
396 pointerDownMapCoords: event.mapCoords
397 };
398
399 this.setState(newState);
400
401 const modeProps = this.getModeProps();
402 this._modeHandler.handleStartDragging(startDraggingEvent, modeProps);
403 };
404
405 _onPointerUp = (event: MjolnirEvent) => {
406 const stopDraggingEvent = {
407 ...event,
408 pointerDownScreenCoords: this.state.pointerDownScreenCoords,
409 pointerDownMapCoords: this.state.pointerDownMapCoords
410 };
411
412 const newState = {
413 isDragging: false,
414 didDrag: false,
415 pointerDownPicks: null,
416 pointerDownScreenCoords: null,
417 pointerDownMapCoords: null
418 };
419
420 this.setState(newState);
421
422 const modeProps = this.getModeProps();
423 this._modeHandler.handleStopDragging(stopDraggingEvent, modeProps);
424 };
425
426 _onPan = (event: BaseEvent) => {
427 const { isDragging } = this.state;
428 if (isDragging) {
429 event.sourceEvent.stopImmediatePropagation();
430 }
431 };
432
433 /* HELPERS */
434 project = (pt: Position) => {
435 const viewport = this._context && this._context.viewport;
436 return viewport && viewport.project(pt);
437 };
438
439 unproject = (pt: Position) => {
440 const viewport = this._context && this._context.viewport;
441 return viewport && viewport.unproject(pt);
442 };
443
444 _getEvent(evt: MjolnirEvent) {
445 const picked = parseEventElement(evt);
446 const screenCoords = getScreenCoords(evt);
447 const mapCoords = this.unproject(screenCoords);
448
449 return {
450 picks: picked ? [picked] : null,
451 screenCoords,
452 mapCoords,
453 sourceEvent: evt
454 };
455 }
456
457 _getHoverState = (event: BaseEvent) => {
458 const object = event.picks && event.picks[0] && event.picks[0].object;
459 if (!object) {
460 return null;
461 }
462
463 return {
464 screenCoords: event.screenCoords,
465 mapCoords: event.mapCoords,
466 ...object
467 };
468 };
469
470 _isDrawing() {
471 const { mode } = this.props;
472 return DRAWING_MODE.findIndex(m => m === mode) >= 0;
473 }
474
475 render(child: any) {
476 return (
477 <MapContext.Consumer>
478 {context => {
479 this._context = context;
480 const viewport = context && context.viewport;
481
482 if (!viewport || viewport.height <= 0 || viewport.width <= 0) {
483 return null;
484 }
485
486 return child;
487 }}
488 </MapContext.Consumer>
489 );
490 }
491}
492
493ModeHandler.displayName = 'ModeHandler';