UNPKG

tmp-react-map-gl-draw

Version:

A lite version editing layer with react

569 lines (496 loc) 16.1 kB
// @flow import React from 'react'; import type { Feature } from '@nebula.gl/edit-modes'; import type { GeoJsonType, RenderState, Id } from './types'; import { RENDER_STATE, RENDER_TYPE, GEOJSON_TYPE, GUIDE_TYPE, ELEMENT_TYPE } from './constants'; import ModeHandler from './mode-handler'; import { getFeatureCoordinates } from './edit-modes/utils'; import { editHandleStyle as defaultEditHandleStyle, featureStyle as defaultFeatureStyle } from './style'; const defaultProps = { ...ModeHandler.defaultProps, clickRadius: 0, featureShape: 'circle', editHandleShape: 'rect', editHandleStyle: defaultEditHandleStyle, featureStyle: defaultFeatureStyle }; export default class Editor extends ModeHandler { static defaultProps = defaultProps; /* HELPERS */ _getPathInScreenCoords(coordinates: any, type: GeoJsonType) { if (coordinates.length === 0) { return ''; } const screenCoords = coordinates.map(p => this.project(p)); let pathString = ''; switch (type) { case GEOJSON_TYPE.POINT: return screenCoords; case GEOJSON_TYPE.LINE_STRING: pathString = screenCoords.map(p => `${p[0]},${p[1]}`).join('L'); return `M ${pathString}`; case GEOJSON_TYPE.POLYGON: pathString = screenCoords.map(p => `${p[0]},${p[1]}`).join('L'); return `M ${pathString} z`; default: return null; } } _getEditHandleState = (editHandle: Feature, renderState: ?string) => { const { pointerDownPicks, hovered } = this.state; if (renderState) { return renderState; } const editHandleIndex = editHandle.properties.positionIndexes[0]; let draggingEditHandleIndex = null; const pickedObject = pointerDownPicks && pointerDownPicks[0] && pointerDownPicks[0].object; if (pickedObject && pickedObject.guideType === GUIDE_TYPE.EDIT_HANDLE) { draggingEditHandleIndex = pickedObject.index; } if (editHandleIndex === draggingEditHandleIndex) { return RENDER_STATE.SELECTED; } if (hovered && hovered.type === ELEMENT_TYPE.EDIT_HANDLE) { if (hovered.index === editHandleIndex) { return RENDER_STATE.HOVERED; } // cursor hovered on first vertex when drawing polygon if ( hovered.index === 0 && editHandle.properties.guideType === GUIDE_TYPE.CURSOR_EDIT_HANDLE ) { return RENDER_STATE.CLOSING; } } return RENDER_STATE.INACTIVE; }; _getFeatureRenderState = (index: number, renderState: ?RenderState) => { const { hovered } = this.state; const selectedFeatureIndex = this._getSelectedFeatureIndex(); if (renderState) { return renderState; } if (index === selectedFeatureIndex) { return RENDER_STATE.SELECTED; } if (hovered && hovered.type === ELEMENT_TYPE.FEATURE && hovered.featureIndex === index) { return RENDER_STATE.HOVERED; } return RENDER_STATE.INACTIVE; }; _getStyleProp = (styleProp: any, params: any) => { return typeof styleProp === 'function' ? styleProp(params) : styleProp; }; /* RENDER */ /* eslint-disable max-params */ _renderEditHandle = (editHandle: Feature, feature: Feature) => { /* eslint-enable max-params */ const coordinates = getFeatureCoordinates(editHandle); const p = this.project(coordinates && coordinates[0]); if (!p) { return null; } const { properties: { featureIndex, positionIndexes } } = editHandle; const { clickRadius, editHandleShape, editHandleStyle } = this.props; const index = positionIndexes[0]; const shape = this._getStyleProp(editHandleShape, { feature: feature || editHandle, index, featureIndex, state: this._getEditHandleState(editHandle) }); let style = this._getStyleProp(editHandleStyle, { feature: feature || editHandle, index, featureIndex, shape, state: this._getEditHandleState(editHandle) }); // disable events for cursor editHandle if (editHandle.properties.guideType === GUIDE_TYPE.CURSOR_EDIT_HANDLE) { style = { ...style, // disable pointer events for cursor pointerEvents: 'none' }; } const elemKey = `${ELEMENT_TYPE.EDIT_HANDLE}.${featureIndex}.${index}`; // first <circle|rect> is to make path easily interacted with switch (shape) { case 'circle': return ( <g key={elemKey} transform={`translate(${p[0]}, ${p[1]})`}> <circle data-type={ELEMENT_TYPE.EDIT_HANDLE} data-index={index} data-feature-index={featureIndex} key={`${elemKey}.hidden`} style={{ ...style, stroke: 'none', fill: '#000', fillOpacity: 0 }} cx={0} cy={0} r={clickRadius} /> <circle data-type={ELEMENT_TYPE.EDIT_HANDLE} data-index={index} data-feature-index={featureIndex} key={elemKey} style={style} cx={0} cy={0} /> </g> ); case 'rect': return ( <g key={elemKey} transform={`translate(${p[0]}, ${p[1]})`}> <rect data-type={ELEMENT_TYPE.EDIT_HANDLE} data-index={index} data-feature-index={featureIndex} key={`${elemKey}.hidden`} style={{ ...style, height: clickRadius, width: clickRadius, fill: '#000', fillOpacity: 0 }} r={clickRadius} /> <rect data-type={ELEMENT_TYPE.EDIT_HANDLE} data-index={index} data-feature-index={featureIndex} key={`${elemKey}`} style={style} /> </g> ); default: return null; } }; _renderSegment = (featureIndex: Id, index: number, coordinates: number[], style: Object) => { const path = this._getPathInScreenCoords(coordinates, GEOJSON_TYPE.LINE_STRING); const { radius, ...others } = style; const { clickRadius } = this.props; const elemKey = `${ELEMENT_TYPE.SEGMENT}.${featureIndex}.${index}`; return ( <g key={elemKey}> <path key={`${elemKey}.hidden`} data-type={ELEMENT_TYPE.SEGMENT} data-index={index} data-feature-index={featureIndex} style={{ ...others, strokeWidth: clickRadius || radius, opacity: 0 }} d={path} /> <path key={elemKey} data-type={ELEMENT_TYPE.SEGMENT} data-index={index} data-feature-index={featureIndex} style={others} d={path} /> </g> ); }; _renderSegments = (featureIndex: Id, coordinates: number[], style: Object) => { const segments = []; for (let i = 0; i < coordinates.length - 1; i++) { segments.push( this._renderSegment(featureIndex, i, [coordinates[i], coordinates[i + 1]], style) ); } return segments; }; _renderFill = (featureIndex: Id, coordinates: number[], style: Object) => { const path = this._getPathInScreenCoords(coordinates, GEOJSON_TYPE.POLYGON); return ( <path key={`${ELEMENT_TYPE.FILL}.${featureIndex}`} data-type={ELEMENT_TYPE.FILL} data-feature-index={featureIndex} style={{ ...style, stroke: 'none' }} d={path} /> ); }; _renderTentativeFeature = (feature: Feature, cursorEditHandle: Feature) => { const { featureStyle } = this.props; const { geometry: { coordinates }, properties: { renderType } } = feature; if (!coordinates || coordinates.length < 2) { return null; } // >= 2 coordinates const firstCoords = coordinates[0]; const lastCoords = coordinates[coordinates.length - 1]; const uncommittedStyle = this._getStyleProp(featureStyle, { feature, index: null, state: RENDER_STATE.UNCOMMITTED }); let committedPath; let uncommittedPath; let closingPath; const fill = this._renderFill('tentative', coordinates, uncommittedStyle); switch (renderType) { case RENDER_TYPE.LINE_STRING: case RENDER_TYPE.POLYGON: const committedStyle = this._getStyleProp(featureStyle, { feature, state: RENDER_STATE.SELECTED }); if (cursorEditHandle) { const cursorCoords = coordinates[coordinates.length - 2]; committedPath = this._renderSegments( 'tentative', coordinates.slice(0, coordinates.length - 1), committedStyle ); uncommittedPath = this._renderSegment( 'tentative-uncommitted', coordinates.length - 2, [cursorCoords, lastCoords], uncommittedStyle ); } else { committedPath = this._renderSegments('tentative', coordinates, committedStyle); } if (renderType === RENDER_TYPE.POLYGON) { const closingStyle = this._getStyleProp(featureStyle, { feature, index: null, state: RENDER_STATE.CLOSING }); closingPath = this._renderSegment( 'tentative-closing', coordinates.length - 1, [lastCoords, firstCoords], closingStyle ); } break; case RENDER_TYPE.RECTANGLE: uncommittedPath = this._renderSegments( 'tentative', [...coordinates, firstCoords], uncommittedStyle ); break; default: } return [fill, committedPath, uncommittedPath, closingPath].filter(Boolean); }; _renderGuides = ({ tentativeFeature, editHandles }: Object) => { const features = this.getFeatures(); const cursorEditHandle = editHandles.find( f => f.properties.guideType === GUIDE_TYPE.CURSOR_EDIT_HANDLE ); return ( <g key="feature-guides"> {tentativeFeature && this._renderTentativeFeature(tentativeFeature, cursorEditHandle)} {editHandles && editHandles.map(editHandle => { const feature = (features && features[editHandle.properties.featureIndex]) || tentativeFeature; return this._renderEditHandle(editHandle, feature); })} </g> ); }; _renderPoint = (feature: Feature, index: number, path: string) => { const renderState = this._getFeatureRenderState(index); const { featureStyle, featureShape, clickRadius } = this.props; const shape = this._getStyleProp(featureShape, { feature, index, state: renderState }); const style = this._getStyleProp(featureStyle, { feature, index, state: renderState }); const elemKey = `feature.${index}`; if (shape === 'rect') { return ( <g key={elemKey} transform={`translate(${path[0][0]}, ${path[0][1]})`}> <rect data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={`${elemKey}.hidden`} style={{ ...style, width: clickRadius, height: clickRadius, fill: '#000', fillOpacity: 0 }} /> <rect data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={elemKey} style={style} /> </g> ); } return ( <g key={`feature.${index}`} transform={`translate(${path[0][0]}, ${path[0][1]})`}> <circle data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={`${elemKey}.hidden`} style={{ ...style, opacity: 0 }} cx={0} cy={0} r={clickRadius} /> <circle data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={elemKey} style={style} cx={0} cy={0} /> </g> ); }; _renderPath = (feature: Feature, index: number, path: string) => { const { featureStyle, clickRadius } = this.props; const selectedFeatureIndex = this._getSelectedFeatureIndex(); const selected = index === selectedFeatureIndex; const renderState = this._getFeatureRenderState(index); const style = this._getStyleProp(featureStyle, { feature, index, state: renderState }); const elemKey = `feature.${index}`; if (selected) { return ( <g key={elemKey}>{this._renderSegments(index, feature.geometry.coordinates, style)}</g> ); } // first <path> is to make path easily interacted with return ( <g key={elemKey}> <path data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={`${elemKey}.hidden`} style={{ ...style, strokeWidth: clickRadius, opacity: 0 }} d={path} /> <path data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={elemKey} style={style} d={path} /> </g> ); }; _renderPolygon = (feature: Feature, index: number, path: string) => { const { featureStyle } = this.props; const selectedFeatureIndex = this._getSelectedFeatureIndex(); const selected = index === selectedFeatureIndex; const renderState = this._getFeatureRenderState(index); const style = this._getStyleProp(featureStyle, { feature, index, state: renderState }); const elemKey = `feature.${index}`; if (selected) { const coordinates = getFeatureCoordinates(feature); if (!coordinates) { return null; } return ( <g key={elemKey}> {this._renderFill(index, coordinates, style)} {this._renderSegments(index, coordinates, style)} </g> ); } return ( <path data-type={ELEMENT_TYPE.FEATURE} data-feature-index={index} key={elemKey} style={style} d={path} /> ); }; _renderFeature = (feature: Feature, index: number) => { const coordinates = getFeatureCoordinates(feature); if (!coordinates || !coordinates.length) { return null; } const { properties: { renderType }, geometry: { type } } = feature; const path = this._getPathInScreenCoords(coordinates, type); if (!path) { return null; } switch (renderType) { case RENDER_TYPE.POINT: return this._renderPoint(feature, index, path); case RENDER_TYPE.LINE_STRING: return this._renderPath(feature, index, path); case RENDER_TYPE.POLYGON: case RENDER_TYPE.RECTANGLE: return this._renderPolygon(feature, index, path); default: return null; } }; _renderCanvas = () => { const features = this.getFeatures(); const guides = this._modeHandler && this._modeHandler.getGuides(this.getModeProps()); return ( <svg key="draw-canvas" width="100%" height="100%"> {features && features.length > 0 && <g key="feature-group">{features.map(this._renderFeature)}</g>} {guides && <g key="feature-guides">{this._renderGuides(guides)}</g>} </svg> ); }; _renderEditor = () => { const viewport = (this._context && this._context.viewport) || {}; if(!this._context) return null const { style } = this.props; const { width, height } = viewport; return ( <div id="editor" style={{ width, height, ...style }} ref={_ => { this._containerRef = _; }} > {this._renderCanvas()} </div> ); }; render() { return super.render(this._renderEditor()); } }