// deck.gl-community // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import bbox from '@turf/bbox'; import turfCentroid from '@turf/centroid'; import turfBearing from '@turf/bearing'; import bboxPolygon from '@turf/bbox-polygon'; import {point, featureCollection} from '@turf/helpers'; import {polygonToLine} from '@turf/polygon-to-line'; import {coordEach} from '@turf/meta'; import turfDistance from '@turf/distance'; import turfTransformScale from '@turf/transform-scale'; import {getCoord, getGeom} from '@turf/invariant'; import {FeatureCollection, Position, SimpleFeatureCollection} from '../utils/geojson-types'; import { ModeProps, PointerMoveEvent, StartDraggingEvent, StopDraggingEvent, DraggingEvent, EditHandleFeature, GuideFeatureCollection } from './types'; import {getPickedEditHandle} from './utils'; import {GeoJsonEditMode} from './geojson-edit-mode'; import {ImmutableFeatureCollection} from './immutable-feature-collection'; export class ScaleMode extends GeoJsonEditMode { _geometryBeingScaled: SimpleFeatureCollection | null | undefined; _selectedEditHandle: EditHandleFeature | null | undefined; _cornerGuidePoints: Array = []; _cursor: string | null | undefined; _isScaling = false; _isSinglePointGeometrySelected = (geometry: FeatureCollection | null | undefined): boolean => { const {features} = geometry || {}; if (Array.isArray(features) && features.length === 1) { const {type}: {type: string} = getGeom(features[0]); return type === 'Point'; } return false; }; _getOppositeScaleHandle = (selectedHandle: EditHandleFeature): EditHandleFeature | null => { const selectedHandleIndex = selectedHandle && selectedHandle.properties && Array.isArray(selectedHandle.properties.positionIndexes) && selectedHandle.properties.positionIndexes[0]; if (typeof selectedHandleIndex !== 'number') { return null; } const guidePointCount = this._cornerGuidePoints.length; const oppositeIndex = (selectedHandleIndex + guidePointCount / 2) % guidePointCount; return ( this._cornerGuidePoints.find((p) => { if (!Array.isArray(p.properties.positionIndexes)) { return false; } return p.properties.positionIndexes[0] === oppositeIndex; }) || null ); }; _getUpdatedData = ( props: ModeProps, editedData: SimpleFeatureCollection ) => { let updatedData = new ImmutableFeatureCollection(props.data); const selectedIndexes = props.selectedIndexes; for (let i = 0; i < selectedIndexes.length; i++) { const selectedIndex = selectedIndexes[i]; const movedFeature = editedData.features[i]; updatedData = updatedData.replaceGeometry(selectedIndex, movedFeature.geometry); } return updatedData.getObject(); }; isEditHandleSelected = (): boolean => Boolean(this._selectedEditHandle); getScaleAction = ( startDragPoint: Position, currentPoint: Position, editType: string, props: ModeProps ) => { if (!this._selectedEditHandle) { return null; } const oppositeHandle = this._getOppositeScaleHandle(this._selectedEditHandle); const origin = getCoord(oppositeHandle) as Position; const scaleFactor = getScaleFactor(origin, startDragPoint, currentPoint); const scaledFeatures = turfTransformScale(this._geometryBeingScaled, scaleFactor, {origin}); return { updatedData: this._getUpdatedData(props, scaledFeatures), editType, editContext: { featureIndexes: props.selectedIndexes } }; }; updateCursor = (props: ModeProps) => { if (this._selectedEditHandle) { if (this._cursor) { props.onUpdateCursor(this._cursor); } const cursorGeometry = this.getSelectedFeaturesAsFeatureCollection(props); // Get resize cursor direction from the hovered scale editHandle (e.g. nesw or nwse) const centroid = turfCentroid(cursorGeometry); const bearing = turfBearing(centroid, this._selectedEditHandle); const positiveBearing = bearing < 0 ? bearing + 180 : bearing; if ( (positiveBearing >= 0 && positiveBearing <= 90) || (positiveBearing >= 180 && positiveBearing <= 270) ) { this._cursor = 'nesw-resize'; props.onUpdateCursor('nesw-resize'); } else { this._cursor = 'nwse-resize'; props.onUpdateCursor('nwse-resize'); } } else { props.onUpdateCursor(null); this._cursor = null; } }; handlePointerMove(event: PointerMoveEvent, props: ModeProps) { if (!this._isScaling) { const selectedEditHandle = getPickedEditHandle(event.picks); this._selectedEditHandle = selectedEditHandle && selectedEditHandle.properties.editHandleType === 'scale' ? selectedEditHandle : null; this.updateCursor(props); } } handleStartDragging(event: StartDraggingEvent, props: ModeProps) { if (this._selectedEditHandle) { event.cancelPan(); this._isScaling = true; this._geometryBeingScaled = this.getSelectedFeaturesAsFeatureCollection(props); } } handleDragging(event: DraggingEvent, props: ModeProps) { if (!this._isScaling) { return; } props.onUpdateCursor(this._cursor); const scaleAction = this.getScaleAction( event.pointerDownMapCoords, event.mapCoords, 'scaling', props ); if (scaleAction) { props.onEdit(scaleAction); } event.cancelPan(); } handleStopDragging(event: StopDraggingEvent, props: ModeProps) { if (this._isScaling) { // Scale the geometry const scaleAction = this.getScaleAction( event.pointerDownMapCoords, event.mapCoords, 'scaled', props ); if (scaleAction) { props.onEdit(scaleAction); } props.onUpdateCursor(null); this._geometryBeingScaled = null; this._selectedEditHandle = null; this._cursor = null; this._isScaling = false; } } getGuides(props: ModeProps): GuideFeatureCollection { this._cornerGuidePoints = []; const selectedGeometry = this.getSelectedFeaturesAsFeatureCollection(props); // Add buffer to the enveloping box if a single Point feature is selected if (this._isSinglePointGeometrySelected(selectedGeometry)) { return {type: 'FeatureCollection', features: []}; } const boundingBox = bboxPolygon(bbox(selectedGeometry)); boundingBox.properties.mode = 'scale'; const cornerGuidePoints: EditHandleFeature[] = []; coordEach(boundingBox, (coord, coordIndex) => { if (coordIndex < 4) { // Get corner midpoint guides from the enveloping box const cornerPoint = point(coord, { guideType: 'editHandle', editHandleType: 'scale', positionIndexes: [coordIndex] }); cornerGuidePoints.push(cornerPoint as EditHandleFeature); } }); this._cornerGuidePoints = cornerGuidePoints; // @ts-expect-error turf types diff return featureCollection([polygonToLine(boundingBox), ...this._cornerGuidePoints]); } } function getScaleFactor(centroid: Position, startDragPoint: Position, currentPoint: Position) { const startDistance = turfDistance(centroid, startDragPoint); const endDistance = turfDistance(centroid, currentPoint); return endDistance / startDistance; }