mapbox-gl-draw-bezier-curve-mode
Version:
A custom mode for MapboxGL-Draw to draw and edit bezier curves.
330 lines (284 loc) • 13.1 kB
JavaScript
import MapboxDraw from '@mapbox/mapbox-gl-draw';
const Constants = MapboxDraw.constants;
const createSupplementaryPoints = MapboxDraw.lib.createSupplementaryPoints;
const moveFeatures = MapboxDraw.lib.moveFeatures;
import dragBezierPoints from '../utils/dragBezierPoints';
import createSupplementaryPointsForBezier from '../utils/createSupplementaryPointsForBezier';
import copyBezierGroupToClipboard from '../utils/copyBezierGroupToClipboard';
import {isCtrlCDown} from '../utils/additional_selectors';
import BezierGroup from '../utils/BezierGroup';
const SimpleSelectModeBezierOverride = MapboxDraw.modes.simple_select;
// Intercept simple_select.onSetup to clear any selectedCoordinates for Bezier features
const originalOnSetup_Simple = MapboxDraw.modes.simple_select.onSetup;
SimpleSelectModeBezierOverride.onSetup = function(opts) {
const state = originalOnSetup_Simple.call(this, opts || {});
const selected = this.getSelected ? this.getSelected() : [];
const hasBezier = selected && selected.some(f => f && f.properties && (f.properties.user_bezierGroup || f.properties.bezierGroup));
if (hasBezier) {
this.setSelectedCoordinates([]);
if (state && Array.isArray(state.selectedCoordPaths)) state.selectedCoordPaths = [];
}
return state;
};
// Keep a handle to original handlers to fallback if needed
const originalOnVertex_Simple = MapboxDraw.modes.simple_select.onVertex;
const originalOnMidpoint_Simple = MapboxDraw.modes.simple_select.onMidpoint;
const originalClickOnVertex_Simple = MapboxDraw.modes.simple_select.clickOnVertex;
// Short-circuit selection for Bezier supplementary points to avoid Draw's Polygon coord paths
SimpleSelectModeBezierOverride.onVertex = function(state, e) {
const props = e && e.featureTarget ? e.featureTarget.properties : null;
const parentId = props && props.parent;
const parent = parentId ? this.getFeature(parentId) : null;
const isBezier = !!(parent && parent.properties && (parent.properties.bezierGroup || parent.properties.user_bezierGroup));
if (isBezier) {
// Prevent Draw from computing polygon coord paths
this.setSelected([parent.id]);
this.setSelectedCoordinates([]);
if (state && Array.isArray(state.selectedCoordPaths)) state.selectedCoordPaths = [];
const payload = props && props.coord_path ? { featureId: parent.id, coordPath: props.coord_path } : { featureId: parent.id };
setTimeout(() => { this.changeMode(Constants.modes.DIRECT_SELECT, payload); }, 0);
return;
}
return originalOnVertex_Simple.call(this, state, e);
};
SimpleSelectModeBezierOverride.onMidpoint = function(state, e) {
const props = e && e.featureTarget ? e.featureTarget.properties : null;
const parentId = props && props.parent;
const parent = parentId ? this.getFeature(parentId) : null;
const isBezier = !!(parent && parent.properties && (parent.properties.bezierGroup || parent.properties.user_bezierGroup));
if (isBezier) {
this.setSelected([parent.id]);
this.setSelectedCoordinates([]);
const payload = props && props.coord_path ? { featureId: parent.id, coordPath: props.coord_path } : { featureId: parent.id };
setTimeout(() => { this.changeMode(Constants.modes.DIRECT_SELECT, payload); }, 0);
return;
}
return originalOnMidpoint_Simple.call(this, state, e);
};
// Intercept Draw's internal clickOnVertex (used by simple_select on polygon vertices)
SimpleSelectModeBezierOverride.clickOnVertex = function(state, e) {
const props = e && e.featureTarget ? e.featureTarget.properties : null;
const parentId = props && props.parent;
const parent = parentId ? this.getFeature(parentId) : null;
const isBezier = !!(parent && parent.properties && (parent.properties.bezierGroup || parent.properties.user_bezierGroup));
if (isBezier) {
try {
this.setSelected([parent.id]);
this.setSelectedCoordinates([]);
if (state && Array.isArray(state.selectedCoordPaths)) state.selectedCoordPaths = [];
const payload = props && props.coord_path ? { featureId: parent.id, coordPath: props.coord_path } : { featureId: parent.id };
setTimeout(() => { this.changeMode(Constants.modes.DIRECT_SELECT, payload); }, 0);
return;
} catch (err) {
return;
}
}
return originalClickOnVertex_Simple.call(this, state, e);
};
// Also intercept generic onClick to capture cases where Draw routes differently
const originalOnClick_Simple = MapboxDraw.modes.simple_select.onClick;
SimpleSelectModeBezierOverride.onClick = function(state, e) {
const props = e && e.featureTarget ? e.featureTarget.properties : null;
const parentId = props && props.parent;
const parent = parentId ? this.getFeature(parentId) : null;
const isBezier = !!(parent && parent.properties && (parent.properties.bezierGroup || parent.properties.user_bezierGroup));
if (isBezier && (props && (props.meta === 'vertex' || props.meta === 'midpoint' || props.bezierPoint))) {
try {
this.setSelected([parent.id]);
this.setSelectedCoordinates([]);
if (state && Array.isArray(state.selectedCoordPaths)) state.selectedCoordPaths = [];
const payload = props && props.coord_path ? { featureId: parent.id, coordPath: props.coord_path } : { featureId: parent.id };
setTimeout(() => { this.changeMode(Constants.modes.DIRECT_SELECT, payload); }, 0);
return;
} catch (err) {
return;
}
}
return originalOnClick_Simple ? originalOnClick_Simple.call(this, state, e) : undefined;
};
// --- Debug-friendly override: resolve selected coordinates for Bezier features ---
const originalPathsToCoordinates_Simple = MapboxDraw.modes.simple_select.pathsToCoordinates;
SimpleSelectModeBezierOverride.pathsToCoordinates = function(featureId, selectedCoordPaths) {
const feature = this.getFeature(featureId);
const bgProps = feature && feature.properties ? (feature.properties.bezierGroup || feature.properties.user_bezierGroup) : null;
if (bgProps) {
const bg = new BezierGroup(bgProps.bezierCurves);
return (selectedCoordPaths || [])
.map((cp) => {
const res = bg.getBezierCurveAndNodeFromCoordPath(cp);
return res && res.node && res.node.coords ? res.node.coords : null;
})
.filter(Boolean);
}
return originalPathsToCoordinates_Simple.call(this, featureId, selectedCoordPaths);
};
SimpleSelectModeBezierOverride.dragMove = function(state, e) {
// Dragging when drag move is enabled
state.dragMoving = true;
e.originalEvent.stopPropagation();
const delta = {
lng: e.lngLat.lng - state.dragMoveLocation.lng,
lat: e.lngLat.lat - state.dragMoveLocation.lat
};
moveFeatures(this.getSelected(), delta);
// Move bezier control points & handles
dragBezierPoints(this, delta);
state.dragMoveLocation = e.lngLat;
};
SimpleSelectModeBezierOverride.toDisplayFeatures = function(state, geojson, display) {
geojson.properties.active = (this.isSelected(geojson.properties.id)) ? Constants.activeStates.ACTIVE : Constants.activeStates.INACTIVE;
display(geojson);
this.fireActionable();
if (geojson.properties.active !== Constants.activeStates.ACTIVE ||
geojson.geometry.type === Constants.geojsonTypes.POINT) return;
// If Bezier curve create supplementary points for bezier points instead
const usingBezier = !!geojson.properties.user_bezierGroup;
if (usingBezier && state && Array.isArray(state.selectedCoordPaths) && state.selectedCoordPaths.length > 0) {
state.selectedCoordPaths = [];
this.setSelectedCoordinates([]);
}
const supplementaryPoints = usingBezier ? createSupplementaryPointsForBezier(geojson) : createSupplementaryPoints(geojson);
if(supplementaryPoints){
supplementaryPoints.forEach(display);
}
};
SimpleSelectModeBezierOverride.onKeyDown = function(state, e) {
if (isCtrlCDown(e)){
copyBezierGroupToClipboard(this.getSelected());
}
}
SimpleSelectModeBezierOverride.onCombineFeatures = function() {
const selectedFeatures = this.getSelected();
if (selectedFeatures.length === 0 || selectedFeatures.length < 2) return;
const featureType = selectedFeatures[0].type.replace('Multi', '');
const isBezierGroup = (selectedFeatures[0].properties.bezierGroup != null);
// Verify all features are of the same type
for (let i = 0; i < selectedFeatures.length; i++) {
const feature = selectedFeatures[i];
// Check non corresponding features
if (feature.type.replace('Multi', '') !== featureType) {
return;
}
// Check BezierCurve compatibility
if(isBezierGroup !== (feature.properties.bezierGroup!=null)){
return;
}
}
// Decide which onCombine we will use
if(isBezierGroup){
return this.onCombineFeaturesBezier();
}
else {
return this.onCombineFeaturesDefault();
}
}
SimpleSelectModeBezierOverride.onCombineFeaturesBezier = function(){
const bezierCurves = [];
const featuresCombined = [];
const selectedFeatures = this.getSelected();
for (let i = 0; i < selectedFeatures.length; i++) {
const feature = selectedFeatures[i];
const bezierGroup = feature.properties.bezierGroup;
// Multi
if (bezierGroup.bezierCurves.length>1) {
bezierGroup.bezierCurves.forEach(bezierCurve => {
bezierCurves.push(bezierCurve);
});
}
// Single
else {
bezierCurves.push(bezierGroup.bezierCurves[0]);
}
featuresCombined.push(feature.toGeoJSON());
}
if (bezierCurves.length > 1) {
const bezierGroup = new BezierGroup(bezierCurves);
const multiFeature = this.newFeature(bezierGroup.geojson);
multiFeature.incomingCoords(bezierGroup.vertices);
multiFeature.properties.bezierGroup=bezierGroup;
this.addFeature(multiFeature);
this.deleteFeature(this.getSelectedIds(), { silent: true });
this.setSelected([multiFeature.id]);
this.map.fire(Constants.events.COMBINE_FEATURES, {
createdFeatures: [multiFeature.toGeoJSON()],
deletedFeatures: featuresCombined
});
}
this.fireActionable();
}
SimpleSelectModeBezierOverride.onCombineFeaturesDefault = function() {
const selectedFeatures = this.getSelected();
const coordinates = [], featuresCombined = [];
const featureType = selectedFeatures[0].type.replace('Multi', '');
for (let i = 0; i < selectedFeatures.length; i++) {
const feature = selectedFeatures[i];
if (feature.type.includes('Multi')) {
feature.getCoordinates().forEach((subcoords) => {
coordinates.push(subcoords);
});
} else {
coordinates.push(feature.getCoordinates());
}
featuresCombined.push(feature.toGeoJSON());
}
if (featuresCombined.length > 1) {
const multiFeature = this.newFeature({
type: Constants.geojsonTypes.FEATURE,
properties: featuresCombined[0].properties,
geometry: {
type: `Multi${featureType}`,
coordinates
}
});
this.addFeature(multiFeature);
this.deleteFeature(this.getSelectedIds(), { silent: true });
this.setSelected([multiFeature.id]);
this.map.fire(Constants.events.COMBINE_FEATURES, {
createdFeatures: [multiFeature.toGeoJSON()],
deletedFeatures: featuresCombined
});
}
this.fireActionable();
};
SimpleSelectModeBezierOverride.onUncombineFeatures = function() {
const selectedFeatures = this.getSelected();
if (selectedFeatures.length === 0) return;
const createdFeatures = [];
const featuresUncombined = [];
for (let i = 0; i < selectedFeatures.length; i++) {
const feature = selectedFeatures[i];
const bezierGroup = feature.properties.bezierGroup;
if (this.isInstanceOf('MultiFeature', feature)) {
// Bezier curve behaviour
if(bezierGroup){
bezierGroup.bezierCurves.forEach(bezierCurve => {
const newBezierGroup = new BezierGroup([bezierCurve]);
const subFeature = this.newFeature(newBezierGroup.geojson);
this.addFeature(subFeature);
createdFeatures.push(subFeature.toGeoJSON());
this.select([subFeature.id]);
});
this.deleteFeature(feature.id, { silent: true });
}
// Default behaviour
else {
feature.getFeatures().forEach((subFeature) => {
this.addFeature(subFeature);
subFeature.properties = feature.properties;
createdFeatures.push(subFeature.toGeoJSON());
this.select([subFeature.id]);
});
this.deleteFeature(feature.id, { silent: true });
}
featuresUncombined.push(feature.toGeoJSON());
}
}
if (createdFeatures.length > 1) {
this.map.fire(Constants.events.UNCOMBINE_FEATURES, {
createdFeatures,
deletedFeatures: featuresUncombined
});
}
this.fireActionable();
};
export default SimpleSelectModeBezierOverride;