Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
291 lines (262 sloc)
9.49 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { point, lineString as toLineString } from '@turf/helpers'; | |
import { | |
recursivelyTraverseNestedArrays, | |
nearestPointOnProjectedLine, | |
nearestPointOnLine, | |
getEditHandlesForGeometry, | |
getPickedEditHandles, | |
getPickedEditHandle, | |
getPickedExistingEditHandle, | |
getPickedIntermediateEditHandle, | |
updateRectanglePosition, | |
NearestPointType, | |
} from '../utils'; | |
import { LineString, Point, Polygon, FeatureCollection, FeatureOf } from '../geojson-types'; | |
import { | |
ModeProps, | |
ClickEvent, | |
PointerMoveEvent, | |
StartDraggingEvent, | |
StopDraggingEvent, | |
DraggingEvent, | |
Viewport, | |
GuideFeatureCollection, | |
EditHandleFeature, | |
} from '../types'; | |
import { GeoJsonEditMode } from './geojson-edit-mode'; | |
import { ImmutableFeatureCollection } from './immutable-feature-collection'; | |
export class ModifyMode extends GeoJsonEditMode { | |
getGuides(props: ModeProps<FeatureCollection>): GuideFeatureCollection { | |
const handles = []; | |
const { data, lastPointerMoveEvent } = props; | |
const { features } = data; | |
const picks = lastPointerMoveEvent && lastPointerMoveEvent.picks; | |
const mapCoords = lastPointerMoveEvent && lastPointerMoveEvent.mapCoords; | |
for (const index of props.selectedIndexes) { | |
if (index < features.length) { | |
const { geometry } = features[index]; | |
handles.push(...getEditHandlesForGeometry(geometry, index)); | |
} else { | |
console.warn(`selectedFeatureIndexes out of range ${index}`); // eslint-disable-line no-console,no-undef | |
} | |
} | |
// intermediate edit handle | |
if (picks && picks.length && mapCoords) { | |
const existingEditHandle = getPickedExistingEditHandle(picks); | |
// don't show intermediate point when too close to an existing edit handle | |
const featureAsPick = !existingEditHandle && picks.find((pick) => !pick.isGuide); | |
// is the feature in the pick selected | |
if ( | |
featureAsPick && | |
!featureAsPick.object.geometry.type.includes('Point') && | |
!( | |
props.modeConfig?.lockRectangles && featureAsPick.object.properties.shape === 'Rectangle' | |
) && | |
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-ignore | |
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.' | |
); | |
} | |
return nearestPointOnLine(line, inPoint, viewport); | |
} | |
handleClick(event: ClickEvent, props: ModeProps<FeatureCollection>) { | |
const pickedExistingHandle = getPickedExistingEditHandle(event.picks); | |
const pickedIntermediateHandle = getPickedIntermediateEditHandle(event.picks); | |
if (pickedExistingHandle) { | |
const { featureIndex, positionIndexes } = pickedExistingHandle.properties; | |
let updatedData; | |
try { | |
updatedData = new ImmutableFeatureCollection(props.data) | |
.removePosition(featureIndex, positionIndexes) | |
.getObject(); | |
} catch (ignored) { | |
// This happens if user attempts to remove the last point | |
} | |
if (updatedData) { | |
props.onEdit({ | |
updatedData, | |
editType: 'removePosition', | |
editContext: { | |
featureIndexes: [featureIndex], | |
positionIndexes, | |
position: pickedExistingHandle.geometry.coordinates, | |
}, | |
}); | |
} | |
} else if (pickedIntermediateHandle) { | |
const { featureIndex, positionIndexes } = pickedIntermediateHandle.properties; | |
const feature = props.data.features[featureIndex]; | |
const canAddPosition = !( | |
props.modeConfig?.lockRectangles && feature?.properties.shape === 'Rectangle' | |
); | |
if (canAddPosition) { | |
const updatedData = new ImmutableFeatureCollection(props.data) | |
.addPosition(featureIndex, positionIndexes, pickedIntermediateHandle.geometry.coordinates) | |
.getObject(); | |
if (updatedData) { | |
props.onEdit({ | |
updatedData, | |
editType: 'addPosition', | |
editContext: { | |
featureIndexes: [featureIndex], | |
positionIndexes, | |
position: pickedIntermediateHandle.geometry.coordinates, | |
}, | |
}); | |
} | |
} | |
} | |
} | |
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(); | |
this._dragEditHandle('movePosition', props, editHandle, event); | |
} | |
} | |
_dragEditHandle( | |
editType: string, | |
props: ModeProps<FeatureCollection>, | |
editHandle: EditHandleFeature, | |
event: StopDraggingEvent | DraggingEvent | |
) { | |
const editHandleProperties = editHandle.properties; | |
const editedFeature = props.data.features[editHandleProperties.featureIndex]; | |
let updatedData; | |
if (props.modeConfig?.lockRectangles && editedFeature.properties.shape === 'Rectangle') { | |
const coordinates = updateRectanglePosition( | |
editedFeature as FeatureOf<Polygon>, | |
editHandleProperties.positionIndexes[1], | |
event.mapCoords | |
); | |
updatedData = new ImmutableFeatureCollection(props.data) | |
.replaceGeometry(editHandleProperties.featureIndex, { coordinates, type: 'Polygon' }) | |
.getObject(); | |
} else { | |
updatedData = new ImmutableFeatureCollection(props.data) | |
.replacePosition( | |
editHandleProperties.featureIndex, | |
editHandleProperties.positionIndexes, | |
event.mapCoords | |
) | |
.getObject(); | |
} | |
props.onEdit({ | |
updatedData, | |
editType, | |
editContext: { | |
featureIndexes: [editHandleProperties.featureIndex], | |
positionIndexes: editHandleProperties.positionIndexes, | |
position: event.mapCoords, | |
}, | |
}); | |
} | |
handlePointerMove(event: PointerMoveEvent, props: ModeProps<FeatureCollection>): void { | |
const cursor = this.getCursor(event); | |
props.onUpdateCursor(cursor); | |
} | |
handleStartDragging(event: StartDraggingEvent, props: ModeProps<FeatureCollection>) { | |
const selectedFeatureIndexes = props.selectedIndexes; | |
const editHandle = getPickedIntermediateEditHandle(event.picks); | |
if (selectedFeatureIndexes.length && editHandle) { | |
const editHandleProperties = editHandle.properties; | |
const updatedData = new ImmutableFeatureCollection(props.data) | |
.addPosition( | |
editHandleProperties.featureIndex, | |
editHandleProperties.positionIndexes, | |
event.mapCoords | |
) | |
.getObject(); | |
props.onEdit({ | |
updatedData, | |
editType: 'addPosition', | |
editContext: { | |
featureIndexes: [editHandleProperties.featureIndex], | |
positionIndexes: editHandleProperties.positionIndexes, | |
position: event.mapCoords, | |
}, | |
}); | |
} | |
} | |
handleStopDragging(event: StopDraggingEvent, props: ModeProps<FeatureCollection>) { | |
const selectedFeatureIndexes = props.selectedIndexes; | |
const editHandle = getPickedEditHandle(event.picks); | |
if (selectedFeatureIndexes.length && editHandle) { | |
this._dragEditHandle('finishMovePosition', props, editHandle, event); | |
} | |
} | |
getCursor(event: PointerMoveEvent): string | null | undefined { | |
const picks = (event && event.picks) || []; | |
const handlesPicked = getPickedEditHandles(picks); | |
if (handlesPicked.length) { | |
return 'cell'; | |
} | |
return null; | |
} | |
} |