1 |
|
2 | import { _MapContext as MapContext } from 'react-map-gl';
|
3 | import React, { PureComponent } from 'react';
|
4 | import { ImmutableFeatureCollection } from '@nebula.gl/edit-modes';
|
5 |
|
6 | import type { Feature, Position, EditAction } from '@nebula.gl/edit-modes';
|
7 | import type { MjolnirEvent } from 'mjolnir.js';
|
8 | import type { BaseEvent, EditorProps, EditorState, SelectAction } from './types';
|
9 | import memoize from './memoize';
|
10 |
|
11 | import { DRAWING_MODE, EDIT_TYPE, ELEMENT_TYPE, MODES } from './constants';
|
12 | import { getScreenCoords, isNumeric, parseEventElement } from './edit-modes/utils';
|
13 | import {
|
14 | SelectMode,
|
15 | EditingMode,
|
16 | DrawPointMode,
|
17 | DrawLineStringMode,
|
18 | DrawRectangleMode,
|
19 | DrawPolygonMode,
|
20 | DrawCircleMode,
|
21 | } from './edit-modes';
|
22 |
|
23 | const 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 |
|
34 | const defaultProps = {
|
35 | mode: MODES.READ_ONLY,
|
36 | features: null,
|
37 | onSelect: null,
|
38 | onUpdate: null
|
39 | };
|
40 |
|
41 | const defaultState = {
|
42 | featureCollection: new ImmutableFeatureCollection({
|
43 | type: 'FeatureCollection',
|
44 | features: []
|
45 | }),
|
46 |
|
47 | selectedFeatureIndex: null,
|
48 |
|
49 |
|
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 |
|
62 | export 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 |
|
154 | _getMemorizedFeatureCollection = memoize(({ propsFeatures, stateFeatures }: any) => {
|
155 | const features = propsFeatures || stateFeatures;
|
156 |
|
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 |
|
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 |
|
251 |
|
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 |
|
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 |
|
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 |
|
377 |
|
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 |
|
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 |
|
497 | ModeHandler.displayName = 'ModeHandler';
|