UNPKG

tmp-react-map-gl-draw

Version:

A lite version editing layer with react

494 lines (416 loc) 13.7 kB
// @flow import { _MapContext as MapContext } from 'react-map-gl'; import React, { PureComponent } from 'react'; import { ImmutableFeatureCollection } from '@nebula.gl/edit-modes'; import type { Feature, Position, EditAction } from '@nebula.gl/edit-modes'; import type { MjolnirEvent } from 'mjolnir.js'; import type { BaseEvent, EditorProps, EditorState, SelectAction } from './types'; import memoize from './memoize'; import { DRAWING_MODE, EDIT_TYPE, ELEMENT_TYPE, MODES } from './constants'; import { getScreenCoords, isNumeric, parseEventElement } from './edit-modes/utils'; import { SelectMode, EditingMode, DrawPointMode, DrawLineStringMode, DrawRectangleMode, DrawPolygonMode } from './edit-modes'; const MODE_TO_HANDLER = Object.freeze({ [MODES.READ_ONLY]: null, [MODES.SELECT]: SelectMode, [MODES.EDITING]: EditingMode, [MODES.DRAW_POINT]: DrawPointMode, [MODES.DRAW_PATH]: DrawLineStringMode, [MODES.DRAW_RECTANGLE]: DrawRectangleMode, [MODES.DRAW_POLYGON]: DrawPolygonMode }); const defaultProps = { mode: MODES.READ_ONLY, features: null, onSelect: null, onUpdate: null }; const defaultState = { featureCollection: new ImmutableFeatureCollection({ type: 'FeatureCollection', features: [] }), selectedFeatureIndex: null, // index, isGuide, mapCoords, screenCoords hovered: null, isDragging: false, didDrag: false, lastPointerMoveEvent: null, pointerDownPicks: null, pointerDownScreenCoords: null, pointerDownMapCoords: null }; export default class ModeHandler extends PureComponent<EditorProps, EditorState> { static defaultProps = defaultProps; constructor() { super(); this.state = defaultState; this._eventsRegistered = false; this._events = { anyclick: evt => this._onEvent(this._onClick, evt, true), click: evt => evt.stopImmediatePropagation(), pointermove: evt => this._onEvent(this._onPointerMove, evt, true), pointerdown: evt => this._onEvent(this._onPointerDown, evt, true), pointerup: evt => this._onEvent(this._onPointerUp, evt, true), panmove: evt => this._onEvent(this._onPan, evt, false), panstart: evt => this._onEvent(this._onPan, evt, false), panend: evt => this._onEvent(this._onPan, evt, false) }; } componentDidMount() { this._setupModeHandler(); } componentDidUpdate(prevProps: EditorProps) { if (prevProps.mode !== this.props.mode) { this._clearEditingState(); this._setupModeHandler(); } } componentWillUnmount() { this._degregisterEvents(); } _events: any; _eventsRegistered: boolean; _modeHandler: any; _context: ?MapContext; _containerRef: ?HTMLElement; getFeatures = () => { let featureCollection = this._getFeatureCollection(); featureCollection = featureCollection && featureCollection.getObject(); return featureCollection && featureCollection.features; }; addFeatures = (features: Feature | Feature[]) => { let featureCollection = this._getFeatureCollection(); if (featureCollection) { if (!Array.isArray(features)) { features = [features]; } featureCollection = featureCollection.addFeatures(features); this.setState({ featureCollection }); } }; deleteFeatures = (featureIndexes: number | number[]) => { let featureCollection = this._getFeatureCollection(); const selectedFeatureIndex = this._getSelectedFeatureIndex(); if (featureCollection) { if (!Array.isArray(featureIndexes)) { featureIndexes = [featureIndexes]; } featureCollection = featureCollection.deleteFeatures(featureIndexes); const newState: any = { featureCollection }; if (featureIndexes.findIndex(index => selectedFeatureIndex === index) >= 0) { newState.selectedFeatureIndex = null; } this.setState(newState); } }; getModeProps() { const featureCollection = this._getFeatureCollection(); const { lastPointerMoveEvent } = this.state; const selectedFeatureIndex = this._getSelectedFeatureIndex(); const viewport = this._context && this._context.viewport; return { data: featureCollection, selectedIndexes: [selectedFeatureIndex], lastPointerMoveEvent, viewport, onEdit: this._onEdit }; } /* MEMORIZERS */ _getMemorizedFeatureCollection = memoize(({ propsFeatures, stateFeatures }: any) => { const features = propsFeatures || stateFeatures; // Any changes in ImmutableFeatureCollection will create a new object if (features instanceof ImmutableFeatureCollection) { return features; } if (features && features.type === 'FeatureCollection') { return new ImmutableFeatureCollection({ type: 'FeatureCollection', features: features.features }); } return new ImmutableFeatureCollection({ type: 'FeatureCollection', features: features || [] }); }); _getFeatureCollection = () => { return this._getMemorizedFeatureCollection({ propsFeatures: this.props.features, stateFeatures: this.state.featureCollection }); }; _setupModeHandler = () => { const mode = this.props.mode; if (!mode || mode === MODES.READ_ONLY) { this._degregisterEvents(); this._modeHandler = null; return; } this._registerEvents(); const HandlerClass = MODE_TO_HANDLER[mode]; this._modeHandler = HandlerClass ? new HandlerClass() : null; }; /* EDITING OPERATIONS */ _clearEditingState = () => { this.setState({ selectedFeatureIndex: null, hovered: null, pointerDownPicks: null, pointerDownScreenCoords: null, pointerDownMapCoords: null, isDragging: false, didDrag: false }); }; _getSelectedFeatureIndex = () => { if ('selectedFeatureIndex' in this.props) { return this.props.selectedFeatureIndex; } return this.state.selectedFeatureIndex; }; _getSelectedFeature = (featureIndex: ?number) => { const features = this.getFeatures(); featureIndex = isNumeric(featureIndex) ? featureIndex : this._getSelectedFeatureIndex(); return features[featureIndex]; }; _onSelect = (selected: SelectAction) => { this.setState({ selectedFeatureIndex: selected && selected.selectedFeatureIndex }); if (this.props.onSelect) { this.props.onSelect(selected); } }; _onUpdate = (editAction: EditAction, isInternal: ?boolean) => { const { editType, updatedData, editContext } = editAction; this.setState({ featureCollection: new ImmutableFeatureCollection(updatedData) }); if (this.props.onUpdate && !isInternal) { this.props.onUpdate({ data: updatedData && updatedData.features, editType, editContext }); } }; _onEdit = (editAction: EditAction) => { const { mode } = this.props; const { editType, updatedData } = editAction; switch (editType) { case EDIT_TYPE.MOVE_POSITION: // intermediate feature, do not need forward to application // only need update editor internal state this._onUpdate(editAction, !Boolean(this.props.features)); break; case EDIT_TYPE.ADD_FEATURE: this._onUpdate(editAction); if (mode === MODES.DRAW_PATH) { const context = (editAction.editContext && editAction.editContext[0]) || {}; const { screenCoords, mapCoords } = context; const featureIndex = updatedData.features.length - 1; const selectedFeature = this._getSelectedFeature(featureIndex); this._onSelect({ selectedFeature, selectedFeatureIndex: featureIndex, selectedEditHandleIndex: null, screenCoords, mapCoords }); } break; case EDIT_TYPE.ADD_POSITION: case EDIT_TYPE.REMOVE_POSITION: case EDIT_TYPE.FINISH_MOVE_POSITION: this._onUpdate(editAction); break; default: } }; /* EVENTS */ _degregisterEvents = () => { const eventManager = this._context && this._context.eventManager; if (!this._events || !eventManager) { return; } if (this._eventsRegistered) { eventManager.off(this._events); this._eventsRegistered = false; } }; _registerEvents = () => { const ref = this._containerRef; const eventManager = this._context && this._context.eventManager; if (!this._events || !ref || !eventManager) { return; } if (this._eventsRegistered) { return; } eventManager.on(this._events, ref); this._eventsRegistered = true; }; _onEvent = (handler: Function, evt: MjolnirEvent, stopPropagation: boolean) => { const event = this._getEvent(evt); handler(event); if (stopPropagation) { evt.stopImmediatePropagation(); } }; _onClick = (event: BaseEvent) => { const { mode } = this.props; if (mode === MODES.SELECT || mode === MODES.EDITING) { const { mapCoords, screenCoords } = event; const pickedObject = event.picks && event.picks[0] && event.picks[0].object; if (pickedObject && isNumeric(pickedObject.featureIndex)) { const selectedFeatureIndex = pickedObject.featureIndex; const selectedFeature = this._getSelectedFeature(selectedFeatureIndex); this._onSelect({ selectedFeature, selectedFeatureIndex, selectedEditHandleIndex: pickedObject.type === ELEMENT_TYPE.EDIT_HANDLE ? pickedObject.index : null, mapCoords, screenCoords }); } else { this._onSelect({ selectedFeature: null, selectedFeatureIndex: null, selectedEditHandleIndex: null, mapCoords, screenCoords }); } } const modeProps = this.getModeProps(); if(this._modeHandler) this._modeHandler.handleClick(event, modeProps); }; _onPointerMove = (event: BaseEvent) => { // hovering const hovered = this._getHoverState(event); const { isDragging, didDrag, pointerDownPicks, pointerDownScreenCoords, pointerDownMapCoords } = this.state; if (isDragging && !didDrag && pointerDownScreenCoords) { const dx = event.screenCoords[0] - pointerDownScreenCoords[0]; const dy = event.screenCoords[1] - pointerDownScreenCoords[1]; if (dx * dx + dy * dy > 5) { this.setState({ didDrag: true }); } } const pointerMoveEvent = { ...event, isDragging, pointerDownPicks, pointerDownScreenCoords, pointerDownMapCoords }; if (this.state.didDrag) { const modeProps = this.getModeProps(); this._modeHandler.handlePointerMove(pointerMoveEvent, modeProps); } this.setState({ hovered, lastPointerMoveEvent: pointerMoveEvent }); }; _onPointerDown = (event: BaseEvent) => { const pickedObject = event.picks && event.picks[0] && event.picks[0].object; const startDraggingEvent = { ...event, pointerDownScreenCoords: event.screenCoords, pointerDownMapCoords: event.mapCoords }; const newState = { isDragging: pickedObject && isNumeric(pickedObject.featureIndex), pointerDownPicks: event.picks, pointerDownScreenCoords: event.screenCoords, pointerDownMapCoords: event.mapCoords }; this.setState(newState); const modeProps = this.getModeProps(); this._modeHandler.handleStartDragging(startDraggingEvent, modeProps); }; _onPointerUp = (event: MjolnirEvent) => { const stopDraggingEvent = { ...event, pointerDownScreenCoords: this.state.pointerDownScreenCoords, pointerDownMapCoords: this.state.pointerDownMapCoords }; const newState = { isDragging: false, didDrag: false, pointerDownPicks: null, pointerDownScreenCoords: null, pointerDownMapCoords: null }; this.setState(newState); const modeProps = this.getModeProps(); this._modeHandler.handleStopDragging(stopDraggingEvent, modeProps); }; _onPan = (event: BaseEvent) => { const { isDragging } = this.state; if (isDragging) { event.sourceEvent.stopImmediatePropagation(); } }; /* HELPERS */ project = (pt: Position) => { const viewport = this._context && this._context.viewport; return viewport && viewport.project(pt); }; unproject = (pt: Position) => { const viewport = this._context && this._context.viewport; return viewport && viewport.unproject(pt); }; _getEvent(evt: MjolnirEvent) { const picked = parseEventElement(evt); const screenCoords = getScreenCoords(evt); const mapCoords = this.unproject(screenCoords); return { picks: picked ? [picked] : null, screenCoords, mapCoords, sourceEvent: evt }; } _getHoverState = (event: BaseEvent) => { const object = event.picks && event.picks[0] && event.picks[0].object; if (!object) { return null; } return { screenCoords: event.screenCoords, mapCoords: event.mapCoords, ...object }; }; _isDrawing() { const { mode } = this.props; return DRAWING_MODE.findIndex(m => m === mode) >= 0; } render(child: any) { return ( <MapContext.Consumer> {context => { this._context = context; const viewport = context && context.viewport; if (!viewport || viewport.height <= 0 || viewport.width <= 0) { return null; } return child; }} </MapContext.Consumer> ); } } ModeHandler.displayName = 'ModeHandler';