tmp-react-map-gl-draw
Version:
A lite version editing layer with react
494 lines (416 loc) • 13.7 kB
JavaScript
// @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';