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