// deck.gl-community // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {featureCollection} from '@turf/helpers'; import turfUnion from '@turf/union'; import turfDifference from '@turf/difference'; import turfIntersect from '@turf/intersect'; import rewind from '@turf/rewind'; import { EditAction, ClickEvent, PointerMoveEvent, StartDraggingEvent, StopDraggingEvent, DraggingEvent, Pick, Tooltip, ModeProps, GuideFeatureCollection, TentativeFeature } from './types'; import { FeatureCollection, Feature, Polygon, SimpleGeometry, Position, SimpleFeatureCollection, SimpleFeature } from '../utils/geojson-types'; import {getPickedEditHandles, getNonGuidePicks} from './utils'; import {EditMode} from './edit-mode'; import {ImmutableFeatureCollection} from './immutable-feature-collection'; export type GeoJsonEditAction = EditAction; const DEFAULT_GUIDES: GuideFeatureCollection = { type: 'FeatureCollection', features: [] }; const DEFAULT_TOOLTIPS: Tooltip[] = []; // Main interface for `EditMode`s that edit GeoJSON export type GeoJsonEditModeType = EditMode; export interface GeoJsonEditModeConstructor { new (): GeoJsonEditModeType; } export class GeoJsonEditMode implements EditMode { _clickSequence: Position[] = []; getGuides(props: ModeProps): GuideFeatureCollection { return DEFAULT_GUIDES; } getTooltips(props: ModeProps): Tooltip[] { return DEFAULT_TOOLTIPS; } getSelectedFeature(props: ModeProps): SimpleFeature | null | undefined { if (props.selectedIndexes.length === 1) { return props.data.features[props.selectedIndexes[0]]; } return null; } getSelectedGeometry( props: ModeProps ): SimpleGeometry | null | undefined { const feature = this.getSelectedFeature(props); if (feature) { return feature.geometry; } return null; } getSelectedFeaturesAsFeatureCollection( props: ModeProps ): SimpleFeatureCollection { const {features} = props.data; const selectedFeatures = props.selectedIndexes.map((selectedIndex) => features[selectedIndex]); return { type: 'FeatureCollection', features: selectedFeatures }; } getClickSequence(): Position[] { return this._clickSequence; } addClickSequence({mapCoords}: ClickEvent): void { this._clickSequence.push(mapCoords); } resetClickSequence(): void { this._clickSequence = []; } getTentativeGuide(props: ModeProps): TentativeFeature | null | undefined { const guides = this.getGuides(props); return guides.features.find( (f) => f.properties && f.properties.guideType === 'tentative' ) as TentativeFeature; } isSelectionPicked(picks: Pick[], props: ModeProps): boolean { if (!picks.length) return false; const pickedFeatures = getNonGuidePicks(picks).map(({index}) => index); const pickedHandles = getPickedEditHandles(picks).map( ({properties}) => properties.featureIndex ); const pickedIndexes = new Set([...pickedFeatures, ...pickedHandles]); return props.selectedIndexes.some((index) => pickedIndexes.has(index)); } rewindPolygon(feature: SimpleFeature): SimpleFeature { const {geometry} = feature; const isPolygonal = geometry.type === 'Polygon' || geometry.type === 'MultiPolygon'; if (isPolygonal) { return rewind(feature) as SimpleFeature; } return feature; } getAddFeatureAction( featureOrGeometry: SimpleGeometry | Feature, features: SimpleFeatureCollection, featureProperties?: {} ): GeoJsonEditAction { // Unsure why flow can't deal with Geometry type, but there I fixed it const featureOrGeometryAsAny: any = featureOrGeometry; featureProperties = featureProperties || {}; const feature: any = featureOrGeometryAsAny.type === 'Feature' ? featureOrGeometryAsAny : { type: 'Feature', properties: featureProperties, geometry: featureOrGeometryAsAny }; const rewindFeature = this.rewindPolygon(feature); const updatedData = new ImmutableFeatureCollection(features) .addFeature(rewindFeature) .getObject(); return { updatedData, editType: 'addFeature', editContext: { featureIndexes: [updatedData.features.length - 1] } }; } getAddManyFeaturesAction( {features: featuresToAdd}: SimpleFeatureCollection, features: SimpleFeatureCollection ): GeoJsonEditAction { let updatedData = new ImmutableFeatureCollection(features); const initialIndex = updatedData.getObject().features.length; const updatedIndexes: number[] = []; for (const feature of featuresToAdd) { const {properties, geometry} = feature; const geometryAsAny: any = geometry; updatedData = updatedData.addFeature({ type: 'Feature', properties, geometry: geometryAsAny }); updatedIndexes.push(initialIndex + updatedIndexes.length); } return { updatedData: updatedData.getObject(), editType: 'addFeature', editContext: { featureIndexes: updatedIndexes } }; } // eslint-disable-next-line complexity getAddFeatureOrBooleanPolygonAction( featureOrGeometry: Polygon | Feature, props: ModeProps, featureProperties?: {} ): GeoJsonEditAction | null | undefined { const featureOrGeometryAsAny: any = featureOrGeometry; featureProperties = featureProperties || {}; const selectedFeature = this.getSelectedFeature(props); const {modeConfig} = props; if (modeConfig && modeConfig.booleanOperation) { if ( !selectedFeature || (selectedFeature.geometry.type !== 'Polygon' && selectedFeature.geometry.type !== 'MultiPolygon') ) { // eslint-disable-next-line no-console,no-undef console.warn( 'booleanOperation only supported for single Polygon or MultiPolygon selection' ); return null; } const feature = featureOrGeometryAsAny.type === 'Feature' ? featureOrGeometryAsAny : { type: 'Feature', geometry: featureOrGeometryAsAny }; let updatedGeometry; if (modeConfig.booleanOperation === 'union') { updatedGeometry = turfUnion(featureCollection([selectedFeature, feature])); } else if (modeConfig.booleanOperation === 'difference') { updatedGeometry = turfDifference(featureCollection([selectedFeature, feature])); } else if (modeConfig.booleanOperation === 'intersection') { updatedGeometry = turfIntersect(featureCollection([selectedFeature, feature])); } else { // eslint-disable-next-line no-console,no-undef console.warn(`Invalid booleanOperation ${modeConfig.booleanOperation}`); return null; } if (!updatedGeometry) { // eslint-disable-next-line no-console,no-undef console.warn('Canceling edit. Boolean operation erased entire polygon.'); return null; } const featureIndex = props.selectedIndexes[0]; const updatedData = new ImmutableFeatureCollection(props.data) .replaceGeometry(featureIndex, updatedGeometry.geometry) .getObject(); const editAction: GeoJsonEditAction = { updatedData, editType: 'unionGeometry', editContext: { featureIndexes: [featureIndex] } }; return editAction; } return this.getAddFeatureAction(featureOrGeometry, props.data, featureProperties); } createTentativeFeature(props: ModeProps): TentativeFeature | null { return null; } handleClick(event: ClickEvent, props: ModeProps): void {} handleDoubleClick(event: ClickEvent, props: ModeProps): void {} handlePointerMove(event: PointerMoveEvent, props: ModeProps): void { const tentativeFeature = this.createTentativeFeature(props); if (tentativeFeature) { props.onEdit({ updatedData: props.data, editType: 'updateTentativeFeature', editContext: { feature: tentativeFeature } }); } } handleStartDragging(event: StartDraggingEvent, props: ModeProps): void {} handleStopDragging(event: StopDraggingEvent, props: ModeProps): void {} handleDragging(event: DraggingEvent, props: ModeProps): void {} handleKeyUp(event: KeyboardEvent, props: ModeProps): void { if (event.key === 'Escape') { this.resetClickSequence(); props.onEdit({ // Because the new drawing feature is dropped, so the data will keep as the same. updatedData: props.data, editType: 'cancelFeature', editContext: {} }); } } } export function getIntermediatePosition(position1: Position, position2: Position): Position { const intermediatePosition: Position = [ (position1[0] + position2[0]) / 2.0, (position1[1] + position2[1]) / 2.0 ]; return intermediatePosition; }