UNPKG

16.1 kBJavaScriptView Raw
1// @flow
2
3import React from 'react';
4
5import type { Feature } from '@nebula.gl/edit-modes';
6import type { GeoJsonType, RenderState, Id } from './types';
7
8import { RENDER_STATE, RENDER_TYPE, GEOJSON_TYPE, GUIDE_TYPE, ELEMENT_TYPE } from './constants';
9import ModeHandler from './mode-handler';
10import { getFeatureCoordinates } from './edit-modes/utils';
11
12import {
13 editHandleStyle as defaultEditHandleStyle,
14 featureStyle as defaultFeatureStyle
15} from './style';
16
17const defaultProps = {
18 ...ModeHandler.defaultProps,
19 clickRadius: 0,
20 featureShape: 'circle',
21 editHandleShape: 'rect',
22 editHandleStyle: defaultEditHandleStyle,
23 featureStyle: defaultFeatureStyle
24};
25
26export default class Editor extends ModeHandler {
27 static defaultProps = defaultProps;
28
29 /* HELPERS */
30 _getPathInScreenCoords(coordinates: any, type: GeoJsonType) {
31 if (coordinates.length === 0) {
32 return '';
33 }
34
35 const screenCoords = coordinates.map(p => this.project(p));
36
37 let pathString = '';
38 switch (type) {
39 case GEOJSON_TYPE.POINT:
40 return screenCoords;
41
42 case GEOJSON_TYPE.LINE_STRING:
43 pathString = screenCoords.map(p => `${p[0]},${p[1]}`).join('L');
44 return `M ${pathString}`;
45
46 case GEOJSON_TYPE.POLYGON:
47 case GEOJSON_TYPE.CIRCLE:
48 pathString = screenCoords.map(p => `${p[0]},${p[1]}`).join('L');
49 return `M ${pathString} z`;
50
51 default:
52 return null;
53 }
54 }
55
56 _getEditHandleState = (editHandle: Feature, renderState: ?string) => {
57 const { pointerDownPicks, hovered } = this.state;
58
59 if (renderState) {
60 return renderState;
61 }
62
63 const editHandleIndex = editHandle.properties.positionIndexes[0];
64 let draggingEditHandleIndex = null;
65 const pickedObject = pointerDownPicks && pointerDownPicks[0] && pointerDownPicks[0].object;
66 if (pickedObject && pickedObject.guideType === GUIDE_TYPE.EDIT_HANDLE) {
67 draggingEditHandleIndex = pickedObject.index;
68 }
69
70 if (editHandleIndex === draggingEditHandleIndex) {
71 return RENDER_STATE.SELECTED;
72 }
73
74 if (hovered && hovered.type === ELEMENT_TYPE.EDIT_HANDLE) {
75 if (hovered.index === editHandleIndex) {
76 return RENDER_STATE.HOVERED;
77 }
78
79 // cursor hovered on first vertex when drawing polygon
80 if (
81 hovered.index === 0 &&
82 editHandle.properties.guideType === GUIDE_TYPE.CURSOR_EDIT_HANDLE
83 ) {
84 return RENDER_STATE.CLOSING;
85 }
86 }
87
88 return RENDER_STATE.INACTIVE;
89 };
90
91 _getFeatureRenderState = (index: number, renderState: ?RenderState) => {
92 const { hovered } = this.state;
93 const selectedFeatureIndex = this._getSelectedFeatureIndex();
94 if (renderState) {
95 return renderState;
96 }
97
98 if (index === selectedFeatureIndex) {
99 return RENDER_STATE.SELECTED;
100 }
101
102 if (hovered && hovered.type === ELEMENT_TYPE.FEATURE && hovered.featureIndex === index) {
103 return RENDER_STATE.HOVERED;
104 }
105
106 return RENDER_STATE.INACTIVE;
107 };
108
109 _getStyleProp = (styleProp: any, params: any) => {
110 return typeof styleProp === 'function' ? styleProp(params) : styleProp;
111 };
112
113 /* RENDER */
114 /* eslint-disable max-params */
115 _renderEditHandle = (editHandle: Feature, feature: Feature) => {
116 /* eslint-enable max-params */
117 const coordinates = getFeatureCoordinates(editHandle);
118 const p = this.project(coordinates && coordinates[0]);
119 if (!p) {
120 return null;
121 }
122
123 const {
124 properties: { featureIndex, positionIndexes }
125 } = editHandle;
126 const { clickRadius, editHandleShape, editHandleStyle } = this.props;
127
128 const index = positionIndexes[0];
129
130 const shape = this._getStyleProp(editHandleShape, {
131 feature: feature || editHandle,
132 index,
133 featureIndex,
134 state: this._getEditHandleState(editHandle)
135 });
136
137 let style = this._getStyleProp(editHandleStyle, {
138 feature: feature || editHandle,
139 index,
140 featureIndex,
141 shape,
142 state: this._getEditHandleState(editHandle)
143 });
144
145 // disable events for cursor editHandle
146 if (editHandle.properties.guideType === GUIDE_TYPE.CURSOR_EDIT_HANDLE) {
147 style = {
148 ...style,
149 // disable pointer events for cursor
150 pointerEvents: 'none'
151 };
152 }
153
154 const elemKey = `${ELEMENT_TYPE.EDIT_HANDLE}.${featureIndex}.${index}`;
155 // first <circle|rect> is to make path easily interacted with
156 switch (shape) {
157 case 'circle':
158 return (
159 <g key={elemKey} transform={`translate(${p[0]}, ${p[1]})`}>
160 <circle
161 data-type={ELEMENT_TYPE.EDIT_HANDLE}
162 data-index={index}
163 data-feature-index={featureIndex}
164 key={`${elemKey}.hidden`}
165 style={{ ...style, stroke: 'none', fill: '#000', fillOpacity: 0 }}
166 cx={0}
167 cy={0}
168 r={clickRadius}
169 />
170 <circle
171 data-type={ELEMENT_TYPE.EDIT_HANDLE}
172 data-index={index}
173 data-feature-index={featureIndex}
174 key={elemKey}
175 style={style}
176 cx={0}
177 cy={0}
178 />
179 </g>
180 );
181 case 'rect':
182 return (
183 <g key={elemKey} transform={`translate(${p[0]}, ${p[1]})`}>
184 <rect
185 data-type={ELEMENT_TYPE.EDIT_HANDLE}
186 data-index={index}
187 data-feature-index={featureIndex}
188 key={`${elemKey}.hidden`}
189 style={{
190 ...style,
191 height: clickRadius,
192 width: clickRadius,
193 fill: '#000',
194 fillOpacity: 0
195 }}
196 r={clickRadius}
197 />
198 <rect
199 data-type={ELEMENT_TYPE.EDIT_HANDLE}
200 data-index={index}
201 data-feature-index={featureIndex}
202 key={`${elemKey}`}
203 style={style}
204 />
205 </g>
206 );
207
208 default:
209 return null;
210 }
211 };
212
213 _renderSegment = (featureIndex: Id, index: number, coordinates: number[], style: Object) => {
214 const path = this._getPathInScreenCoords(coordinates, GEOJSON_TYPE.LINE_STRING);
215 const { radius, ...others } = style;
216 const { clickRadius } = this.props;
217
218 const elemKey = `${ELEMENT_TYPE.SEGMENT}.${featureIndex}.${index}`;
219 return (
220 <g key={elemKey}>
221 <path
222 key={`${elemKey}.hidden`}
223 data-type={ELEMENT_TYPE.SEGMENT}
224 data-index={index}
225 data-feature-index={featureIndex}
226 style={{
227 ...others,
228 strokeWidth: clickRadius || radius,
229 opacity: 0
230 }}
231 d={path}
232 />
233 <path
234 key={elemKey}
235 data-type={ELEMENT_TYPE.SEGMENT}
236 data-index={index}
237 data-feature-index={featureIndex}
238 style={others}
239 d={path}
240 />
241 </g>
242 );
243 };
244
245 _renderSegments = (featureIndex: Id, coordinates: number[], style: Object) => {
246 const segments = [];
247 for (let i = 0; i < coordinates.length - 1; i++) {
248 segments.push(
249 this._renderSegment(featureIndex, i, [coordinates[i], coordinates[i + 1]], style)
250 );
251 }
252 return segments;
253 };
254
255 _renderFill = (featureIndex: Id, coordinates: number[], style: Object) => {
256 const path = this._getPathInScreenCoords(coordinates, GEOJSON_TYPE.POLYGON);
257 return (
258 <path
259 key={`${ELEMENT_TYPE.FILL}.${featureIndex}`}
260 data-type={ELEMENT_TYPE.FILL}
261 data-feature-index={featureIndex}
262 style={{ ...style, stroke: 'none' }}
263 d={path}
264 />
265 );
266 };
267
268 _renderTentativeFeature = (feature: Feature, cursorEditHandle: Feature) => {
269 const { featureStyle } = this.props;
270 const {
271 geometry: { coordinates },
272 properties: { renderType }
273 } = feature;
274
275 if (!coordinates || coordinates.length < 2) {
276 return null;
277 }
278
279 // >= 2 coordinates
280 const firstCoords = coordinates[0];
281 const lastCoords = coordinates[coordinates.length - 1];
282 const uncommittedStyle = this._getStyleProp(featureStyle, {
283 feature,
284 index: null,
285 state: RENDER_STATE.UNCOMMITTED
286 });
287
288 let committedPath;
289 let uncommittedPath;
290 let closingPath;
291 const fill = this._renderFill('tentative', coordinates, uncommittedStyle);
292
293 switch (renderType) {
294 case RENDER_TYPE.LINE_STRING:
295 case RENDER_TYPE.POLYGON:
296 case RENDER_TYPE.CIRCLE:
297 const committedStyle = this._getStyleProp(featureStyle, {
298 feature,
299 state: RENDER_STATE.SELECTED
300 });
301
302 if (cursorEditHandle) {
303 const cursorCoords = coordinates[coordinates.length - 2];
304 committedPath = this._renderSegments(
305 'tentative',
306 coordinates.slice(0, coordinates.length - 1),
307 committedStyle
308 );
309 uncommittedPath = this._renderSegment(
310 'tentative-uncommitted',
311 coordinates.length - 2,
312 [cursorCoords, lastCoords],
313 uncommittedStyle
314 );
315 } else {
316 committedPath = this._renderSegments('tentative', coordinates, committedStyle);
317 }
318
319 if (renderType === RENDER_TYPE.POLYGON) {
320 const closingStyle = this._getStyleProp(featureStyle, {
321 feature,
322 index: null,
323 state: RENDER_STATE.CLOSING
324 });
325
326 closingPath = this._renderSegment(
327 'tentative-closing',
328 coordinates.length - 1,
329 [lastCoords, firstCoords],
330 closingStyle
331 );
332 }
333
334 break;
335
336 case RENDER_TYPE.RECTANGLE:
337 uncommittedPath = this._renderSegments(
338 'tentative',
339 [...coordinates, firstCoords],
340 uncommittedStyle
341 );
342 break;
343
344 default:
345 }
346
347 return [fill, committedPath, uncommittedPath, closingPath].filter(Boolean);
348 };
349
350 _renderGuides = ({ tentativeFeature, editHandles }: Object) => {
351 const features = this.getFeatures();
352 const cursorEditHandle = editHandles.find(
353 f => f.properties.guideType === GUIDE_TYPE.CURSOR_EDIT_HANDLE
354 );
355 return (
356 <g key="feature-guides">
357 {tentativeFeature && this._renderTentativeFeature(tentativeFeature, cursorEditHandle)}
358 {editHandles &&
359 editHandles.map(editHandle => {
360 const feature =
361 (features && features[editHandle.properties.featureIndex]) || tentativeFeature;
362 return this._renderEditHandle(editHandle, feature);
363 })}
364 </g>
365 );
366 };
367
368 _renderPoint = (feature: Feature, index: number, path: string) => {
369 const renderState = this._getFeatureRenderState(index);
370 const { featureStyle, featureShape, clickRadius } = this.props;
371 const shape = this._getStyleProp(featureShape, { feature, index, state: renderState });
372 const style = this._getStyleProp(featureStyle, { feature, index, state: renderState });
373
374 const elemKey = `feature.${index}`;
375 if (shape === 'rect') {
376 return (
377 <g key={elemKey} transform={`translate(${path[0][0]}, ${path[0][1]})`}>
378 <rect
379 data-type={ELEMENT_TYPE.FEATURE}
380 data-feature-index={index}
381 key={`${elemKey}.hidden`}
382 style={{
383 ...style,
384 width: clickRadius,
385 height: clickRadius,
386 fill: '#000',
387 fillOpacity: 0
388 }}
389 />
390 <rect
391 data-type={ELEMENT_TYPE.FEATURE}
392 data-feature-index={index}
393 key={elemKey}
394 style={style}
395 />
396 </g>
397 );
398 }
399
400 return (
401 <g key={`feature.${index}`} transform={`translate(${path[0][0]}, ${path[0][1]})`}>
402 <circle
403 data-type={ELEMENT_TYPE.FEATURE}
404 data-feature-index={index}
405 key={`${elemKey}.hidden`}
406 style={{
407 ...style,
408 opacity: 0
409 }}
410 cx={0}
411 cy={0}
412 r={clickRadius}
413 />
414 <circle
415 data-type={ELEMENT_TYPE.FEATURE}
416 data-feature-index={index}
417 key={elemKey}
418 style={style}
419 cx={0}
420 cy={0}
421 />
422 </g>
423 );
424 };
425
426 _renderPath = (feature: Feature, index: number, path: string) => {
427 const { featureStyle, clickRadius } = this.props;
428 const selectedFeatureIndex = this._getSelectedFeatureIndex();
429 const selected = index === selectedFeatureIndex;
430 const renderState = this._getFeatureRenderState(index);
431 const style = this._getStyleProp(featureStyle, { feature, index, state: renderState });
432
433 const elemKey = `feature.${index}`;
434 if (selected) {
435 return (
436 <g key={elemKey}>{this._renderSegments(index, feature.geometry.coordinates, style)}</g>
437 );
438 }
439
440 // first <path> is to make path easily interacted with
441 return (
442 <g key={elemKey}>
443 <path
444 data-type={ELEMENT_TYPE.FEATURE}
445 data-feature-index={index}
446 key={`${elemKey}.hidden`}
447 style={{
448 ...style,
449 strokeWidth: clickRadius,
450 opacity: 0
451 }}
452 d={path}
453 />
454 <path
455 data-type={ELEMENT_TYPE.FEATURE}
456 data-feature-index={index}
457 key={elemKey}
458 style={style}
459 d={path}
460 />
461 </g>
462 );
463 };
464
465 _renderPolygon = (feature: Feature, index: number, path: string) => {
466 const { featureStyle } = this.props;
467 const selectedFeatureIndex = this._getSelectedFeatureIndex();
468 const selected = index === selectedFeatureIndex;
469
470 const renderState = this._getFeatureRenderState(index);
471 const style = this._getStyleProp(featureStyle, { feature, index, state: renderState });
472
473 const elemKey = `feature.${index}`;
474 if (selected) {
475 const coordinates = getFeatureCoordinates(feature);
476 if (!coordinates) {
477 return null;
478 }
479 return (
480 <g key={elemKey}>
481 {this._renderFill(index, coordinates, style)}
482 {this._renderSegments(index, coordinates, style)}
483 </g>
484 );
485 }
486
487 return (
488 <path
489 data-type={ELEMENT_TYPE.FEATURE}
490 data-feature-index={index}
491 key={elemKey}
492 style={style}
493 d={path}
494 />
495 );
496 };
497
498 _renderFeature = (feature: Feature, index: number) => {
499 const coordinates = getFeatureCoordinates(feature);
500 if (!coordinates || !coordinates.length) {
501 return null;
502 }
503
504 const {
505 properties: { renderType },
506 geometry: { type }
507 } = feature;
508 const path = this._getPathInScreenCoords(coordinates, type);
509 if (!path) {
510 return null;
511 }
512
513 switch (renderType) {
514 case RENDER_TYPE.POINT:
515 return this._renderPoint(feature, index, path);
516 case RENDER_TYPE.LINE_STRING:
517 return this._renderPath(feature, index, path);
518
519 case RENDER_TYPE.POLYGON:
520 case RENDER_TYPE.RECTANGLE:
521 case RENDER_TYPE.CIRCLE:
522 return this._renderPolygon(feature, index, path);
523
524 default:
525 return null;
526 }
527 };
528
529 _renderCanvas = () => {
530 const features = this.getFeatures();
531 const guides = this._modeHandler && this._modeHandler.getGuides(this.getModeProps());
532
533 return (
534 <svg key="draw-canvas" width="100%" height="100%">
535 {features &&
536 features.length > 0 && <g key="feature-group">{features.map(this._renderFeature)}</g>}
537 {guides && <g key="feature-guides">{this._renderGuides(guides)}</g>}
538 </svg>
539 );
540 };
541
542 _renderEditor = () => {
543 const viewport = (this._context && this._context.viewport) || {};
544 const { style } = this.props;
545 const { width, height } = viewport;
546
547 return (
548 <div
549 id="editor"
550 style={{
551 width,
552 height,
553 ...style
554 }}
555 ref={_ => {
556 this._containerRef = _;
557 }}
558 >
559 {this._renderCanvas()}
560 </div>
561 );
562 };
563
564 render() {
565 return super.render(this._renderEditor());
566 }
567}