modules/edit-modes/src/lib/resize-circle-mode.ts (168 lines of code) (raw):

import nearestPointOnLine from '@turf/nearest-point-on-line'; import { point, lineString as toLineString } from '@turf/helpers'; import circle from '@turf/circle'; import distance from '@turf/distance'; import turfCenter from '@turf/center'; import { recursivelyTraverseNestedArrays, nearestPointOnProjectedLine, getPickedEditHandles, getPickedEditHandle, NearestPointType, } from '../utils'; import { LineString, Point, FeatureCollection, FeatureOf } from '../geojson-types'; import { ModeProps, PointerMoveEvent, StartDraggingEvent, StopDraggingEvent, DraggingEvent, Viewport, EditHandleFeature, GuideFeatureCollection, } from '../types'; import { GeoJsonEditMode } from './geojson-edit-mode'; import { ImmutableFeatureCollection } from './immutable-feature-collection'; export class ResizeCircleMode extends GeoJsonEditMode { _selectedEditHandle: EditHandleFeature | null | undefined; _isResizing = false; getGuides(props: ModeProps<FeatureCollection>): GuideFeatureCollection { const handles = []; const selectedFeatureIndexes = props.selectedIndexes; const { lastPointerMoveEvent } = props; const picks = lastPointerMoveEvent && lastPointerMoveEvent.picks; const mapCoords = lastPointerMoveEvent && lastPointerMoveEvent.mapCoords; // intermediate edit handle if ( picks && picks.length && mapCoords && selectedFeatureIndexes.length === 1 && !this._isResizing ) { const featureAsPick = picks.find((pick) => !pick.isGuide); // is the feature in the pick selected if ( featureAsPick && featureAsPick.object.properties.shape && featureAsPick.object.properties.shape.includes('Circle') && props.selectedIndexes.includes(featureAsPick.index) ) { let intermediatePoint: NearestPointType | null | undefined = null; let positionIndexPrefix = []; const referencePoint = point(mapCoords); // process all lines of the (single) feature recursivelyTraverseNestedArrays( featureAsPick.object.geometry.coordinates, [], (lineString, prefix) => { const lineStringFeature = toLineString(lineString); const candidateIntermediatePoint = this.getNearestPoint( // @ts-expect-error turf types too wide lineStringFeature, referencePoint, props.modeConfig && props.modeConfig.viewport ); if ( !intermediatePoint || candidateIntermediatePoint.properties.dist < intermediatePoint.properties.dist ) { intermediatePoint = candidateIntermediatePoint; positionIndexPrefix = prefix; } } ); // tack on the lone intermediate point to the set of handles if (intermediatePoint) { const { geometry: { coordinates: position }, properties: { index }, } = intermediatePoint; handles.push({ type: 'Feature', properties: { guideType: 'editHandle', editHandleType: 'intermediate', featureIndex: featureAsPick.index, positionIndexes: [...positionIndexPrefix, index + 1], }, geometry: { type: 'Point', coordinates: position, }, }); } } } return { type: 'FeatureCollection', features: handles, }; } // turf.js does not support elevation for nearestPointOnLine getNearestPoint( line: FeatureOf<LineString>, inPoint: FeatureOf<Point>, viewport: Viewport | null | undefined ): NearestPointType { const { coordinates } = line.geometry; if (coordinates.some((coord) => coord.length > 2)) { if (viewport) { // This line has elevation, we need to use alternative algorithm return nearestPointOnProjectedLine(line, inPoint, viewport); } // eslint-disable-next-line no-console,no-undef console.log( 'Editing 3D point but modeConfig.viewport not provided. Falling back to 2D logic.' ); } // @ts-expect-error turf types diff return nearestPointOnLine(line, inPoint); } handleDragging(event: DraggingEvent, props: ModeProps<FeatureCollection>): void { const editHandle = getPickedEditHandle(event.pointerDownPicks); if (editHandle) { // Cancel map panning if pointer went down on an edit handle event.cancelPan(); const editHandleProperties = editHandle.properties; const feature = this.getSelectedFeature(props); // @ts-expect-error turf types diff const center = turfCenter(feature).geometry.coordinates; const numberOfSteps = Object.entries(feature.geometry.coordinates[0]).length - 1; const radius = Math.max(distance(center, event.mapCoords), 0.001); const { steps = numberOfSteps } = {}; const options = { steps }; const updatedFeature = circle(center, radius, options); const geometry = updatedFeature.geometry; const updatedData = new ImmutableFeatureCollection(props.data) // @ts-expect-error turf types diff .replaceGeometry(editHandleProperties.featureIndex, geometry) .getObject(); props.onEdit({ updatedData, editType: 'unionGeometry', editContext: { featureIndexes: [editHandleProperties.featureIndex], }, }); } } handlePointerMove(event: PointerMoveEvent, props: ModeProps<FeatureCollection>): void { if (!this._isResizing) { const selectedEditHandle = getPickedEditHandle(event.picks); this._selectedEditHandle = selectedEditHandle && selectedEditHandle.properties.editHandleType === 'intermediate' ? selectedEditHandle : null; } const cursor = this.getCursor(event); props.onUpdateCursor(cursor); } handleStartDragging(event: StartDraggingEvent, props: ModeProps<FeatureCollection>) { if (this._selectedEditHandle) { this._isResizing = true; } } handleStopDragging(event: StopDraggingEvent, props: ModeProps<FeatureCollection>) { if (this._isResizing) { this._selectedEditHandle = null; this._isResizing = false; } } getCursor(event: PointerMoveEvent): string | null | undefined { const picks = (event && event.picks) || []; const handlesPicked = getPickedEditHandles(picks); if (handlesPicked.length) { return 'cell'; } return null; } }