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 console.log('______ONUPDATE', this.props.onUpdate, isInternal)
234 if (this.props.onUpdate && !isInternal) {
235 this.props.onUpdate({
236 data: updatedData && updatedData.features,
237 editType,
238 editContext
239 });
240 }
241 };
242
243 _onEdit = (editAction: EditAction) => {
244 const { mode } = this.props;
245 const { editType, updatedData } = editAction;
246
247 switch (editType) {
248 case EDIT_TYPE.MOVE_POSITION:
249 // intermediate feature, do not need forward to application
250 // only need update editor internal state
251 this._onUpdate(editAction, Boolean(this.props.features));
252 break;
253 case EDIT_TYPE.ADD_FEATURE:
254 this._onUpdate(editAction);
255 if (mode === MODES.DRAW_PATH) {
256 const context = (editAction.editContext && editAction.editContext[0]) || {};
257 const { screenCoords, mapCoords } = context;
258 const featureIndex = updatedData.features.length - 1;
259 const selectedFeature = this._getSelectedFeature(featureIndex);
260 this._onSelect({
261 selectedFeature,
262 selectedFeatureIndex: featureIndex,
263 selectedEditHandleIndex: null,
264 screenCoords,
265 mapCoords
266 });
267 }
268 break;
269 case EDIT_TYPE.ADD_POSITION:
270 case EDIT_TYPE.REMOVE_POSITION:
271 case EDIT_TYPE.FINISH_MOVE_POSITION:
272 this._onUpdate(editAction);
273 break;
274
275 default:
276 }
277 };
278
279 /* EVENTS */
280 _degregisterEvents = () => {
281 const eventManager = this._context && this._context.eventManager;
282 if (!this._events || !eventManager) {
283 return;
284 }
285
286 if (this._eventsRegistered) {
287 eventManager.off(this._events);
288 this._eventsRegistered = false;
289 }
290 };
291
292 _registerEvents = () => {
293 const ref = this._containerRef;
294 const eventManager = this._context && this._context.eventManager;
295 if (!this._events || !ref || !eventManager) {
296 return;
297 }
298
299 if (this._eventsRegistered) {
300 return;
301 }
302
303 eventManager.on(this._events, ref);
304 this._eventsRegistered = true;
305 };
306
307 _onEvent = (handler: Function, evt: MjolnirEvent, stopPropagation: boolean) => {
308 const event = this._getEvent(evt);
309 handler(event);
310
311 if (stopPropagation) {
312 evt.stopImmediatePropagation();
313 }
314 };
315
316 _onClick = (event: BaseEvent) => {
317 const { mode } = this.props;
318 if (mode === MODES.SELECT || mode === MODES.EDITING) {
319 const { mapCoords, screenCoords } = event;
320 const pickedObject = event.picks && event.picks[0] && event.picks[0].object;
321 if (pickedObject && isNumeric(pickedObject.featureIndex)) {
322 const selectedFeatureIndex = pickedObject.featureIndex;
323 const selectedFeature = this._getSelectedFeature(selectedFeatureIndex);
324 this._onSelect({
325 selectedFeature,
326 selectedFeatureIndex,
327 selectedEditHandleIndex:
328 pickedObject.type === ELEMENT_TYPE.EDIT_HANDLE ? pickedObject.index : null,
329 mapCoords,
330 screenCoords
331 });
332 } else {
333 this._onSelect({
334 selectedFeature: null,
335 selectedFeatureIndex: null,
336 selectedEditHandleIndex: null,
337 mapCoords,
338 screenCoords
339 });
340 }
341 }
342
343 const modeProps = this.getModeProps();
344 this._modeHandler.handleClick(event, modeProps);
345 };
346
347 _onPointerMove = (event: BaseEvent) => {
348 // hovering
349 const hovered = this._getHoverState(event);
350 const {
351 isDragging,
352 didDrag,
353 pointerDownPicks,
354 pointerDownScreenCoords,
355 pointerDownMapCoords
356 } = this.state;
357
358 if (isDragging && !didDrag && pointerDownScreenCoords) {
359 const dx = event.screenCoords[0] - pointerDownScreenCoords[0];
360 const dy = event.screenCoords[1] - pointerDownScreenCoords[1];
361 if (dx * dx + dy * dy > 5) {
362 this.setState({ didDrag: true });
363 }
364 }
365
366 const pointerMoveEvent = {
367 ...event,
368 isDragging,
369 pointerDownPicks,
370 pointerDownScreenCoords,
371 pointerDownMapCoords
372 };
373
374 if (this.state.didDrag) {
375 const modeProps = this.getModeProps();
376 this._modeHandler.handlePointerMove(pointerMoveEvent, modeProps);
377 }
378
379 this.setState({
380 hovered,
381 lastPointerMoveEvent: pointerMoveEvent
382 });
383 };
384
385 _onPointerDown = (event: BaseEvent) => {
386 const pickedObject = event.picks && event.picks[0] && event.picks[0].object;
387 const startDraggingEvent = {
388 ...event,
389 pointerDownScreenCoords: event.screenCoords,
390 pointerDownMapCoords: event.mapCoords
391 };
392
393 const newState = {
394 isDragging: pickedObject && isNumeric(pickedObject.featureIndex),
395 pointerDownPicks: event.picks,
396 pointerDownScreenCoords: event.screenCoords,
397 pointerDownMapCoords: event.mapCoords
398 };
399
400 this.setState(newState);
401
402 const modeProps = this.getModeProps();
403 this._modeHandler.handleStartDragging(startDraggingEvent, modeProps);
404 };
405
406 _onPointerUp = (event: MjolnirEvent) => {
407 const stopDraggingEvent = {
408 ...event,
409 pointerDownScreenCoords: this.state.pointerDownScreenCoords,
410 pointerDownMapCoords: this.state.pointerDownMapCoords
411 };
412
413 const newState = {
414 isDragging: false,
415 didDrag: false,
416 pointerDownPicks: null,
417 pointerDownScreenCoords: null,
418 pointerDownMapCoords: null
419 };
420
421 this.setState(newState);
422
423 const modeProps = this.getModeProps();
424 this._modeHandler.handleStopDragging(stopDraggingEvent, modeProps);
425 };
426
427 _onPan = (event: BaseEvent) => {
428 const { isDragging } = this.state;
429 if (isDragging) {
430 event.sourceEvent.stopImmediatePropagation();
431 }
432 };
433
434 /* HELPERS */
435 project = (pt: Position) => {
436 const viewport = this._context && this._context.viewport;
437 return viewport && viewport.project(pt);
438 };
439
440 unproject = (pt: Position) => {
441 const viewport = this._context && this._context.viewport;
442 return viewport && viewport.unproject(pt);
443 };
444
445 _getEvent(evt: MjolnirEvent) {
446 const picked = parseEventElement(evt);
447 const screenCoords = getScreenCoords(evt);
448 const mapCoords = this.unproject(screenCoords);
449
450 return {
451 picks: picked ? [picked] : null,
452 screenCoords,
453 mapCoords,
454 sourceEvent: evt
455 };
456 }
457
458 _getHoverState = (event: BaseEvent) => {
459 const object = event.picks && event.picks[0] && event.picks[0].object;
460 if (!object) {
461 return null;
462 }
463
464 return {
465 screenCoords: event.screenCoords,
466 mapCoords: event.mapCoords,
467 ...object
468 };
469 };
470
471 _isDrawing() {
472 const { mode } = this.props;
473 return DRAWING_MODE.findIndex(m => m === mode) >= 0;
474 }
475
476 render(child: any) {
477 return (
478 <MapContext.Consumer>
479 {context => {
480 this._context = context;
481 const viewport = context && context.viewport;
482
483 if (!viewport || viewport.height <= 0 || viewport.width <= 0) {
484 return null;
485 }
486
487 return child;
488 }}
489 </MapContext.Consumer>
490 );
491 }
492}
493
494ModeHandler.displayName = 'ModeHandler';