UNPKG

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