mapbox-gl-draw-snap-mode
Version:
Snapping mode for mapbox-gl-draw
195 lines (160 loc) • 5.5 kB
JavaScript
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import {
addPointToVertices,
createSnapList,
getGuideFeature,
IDS,
shouldHideGuide,
snap,
} from "./../utils/index.js";
import booleanIntersects from "@turf/boolean-intersects";
const { doubleClickZoom } = MapboxDraw.lib;
const { geojsonTypes, modes, cursors } = MapboxDraw.constants;
const DrawPolygon = MapboxDraw.modes.draw_polygon;
const SnapPolygonMode = { ...DrawPolygon };
SnapPolygonMode.onSetup = function (options) {
const polygon = this.newFeature({
type: geojsonTypes.FEATURE,
properties: {},
geometry: {
type: geojsonTypes.POLYGON,
coordinates: [[]],
},
});
const verticalGuide = this.newFeature(getGuideFeature(IDS.VERTICAL_GUIDE));
const horizontalGuide = this.newFeature(
getGuideFeature(IDS.HORIZONTAL_GUIDE)
);
this.addFeature(polygon);
this.addFeature(verticalGuide);
this.addFeature(horizontalGuide);
const selectedFeatures = this.getSelected();
this.clearSelectedFeatures();
doubleClickZoom.disable(this);
const [snapList, vertices] = createSnapList(
this.map,
this._ctx.api,
polygon,
this._ctx.options.snapOptions?.snapGetFeatures
);
const state = {
map: this.map,
polygon,
currentVertexPosition: 0,
vertices,
snapList,
selectedFeatures,
verticalGuide,
horizontalGuide,
};
/// Adding default options
state.options = Object.assign(this._ctx.options, {
overlap: true,
});
const moveendCallback = () => {
const [snapList, vertices] = createSnapList(
this.map,
this._ctx.api,
polygon,
this._ctx.options.snapOptions?.snapGetFeatures
);
state.vertices = vertices;
state.snapList = snapList;
};
// for removing listener later on close
state["moveendCallback"] = moveendCallback;
const optionsChangedCallback = (options) => {
state.options = options;
};
// for removing listener later on close
state["optionsChangedCallback"] = optionsChangedCallback;
this.map.on("moveend", moveendCallback);
this.map.on("draw.snap.options_changed", optionsChangedCallback);
return state;
};
SnapPolygonMode.onClick = function (state) {
// We save some processing by rounding on click, not mousemove
const lng = state.snappedLng;
const lat = state.snappedLat;
// End the drawing if this click is on the previous position
if (state.currentVertexPosition > 0) {
const lastVertex =
state.polygon.coordinates[0][state.currentVertexPosition - 1];
state.lastVertex = lastVertex;
if (lastVertex[0] === lng && lastVertex[1] === lat) {
return this.changeMode(modes.SIMPLE_SELECT, {
featureIds: [state.polygon.id],
});
}
}
// const point = state.map.project();
addPointToVertices(state.map, state.vertices, { lng, lat });
state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, lng, lat);
state.currentVertexPosition++;
state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, lng, lat);
};
SnapPolygonMode.onMouseMove = function (state, e) {
const { lng, lat } = snap(state, e);
state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, lng, lat);
state.snappedLng = lng;
state.snappedLat = lat;
if (
state.lastVertex &&
state.lastVertex[0] === lng &&
state.lastVertex[1] === lat
) {
this.updateUIClasses({ mouse: cursors.POINTER });
// cursor options:
// ADD: "add"
// DRAG: "drag"
// MOVE: "move"
// NONE: "none"
// POINTER: "pointer"
} else {
this.updateUIClasses({ mouse: cursors.ADD });
}
};
// This is 'extending' DrawPolygon.toDisplayFeatures
SnapPolygonMode.toDisplayFeatures = function (state, geojson, display) {
if (shouldHideGuide(state, geojson)) return;
// This relies on the the state of SnapPolygonMode being similar to DrawPolygon
DrawPolygon.toDisplayFeatures(state, geojson, display);
};
// This is 'extending' DrawPolygon.onStop
SnapPolygonMode.onStop = function (state) {
this.deleteFeature(IDS.VERTICAL_GUIDE, { silent: true });
this.deleteFeature(IDS.HORIZONTAL_GUIDE, { silent: true });
// remove moveend callback
this.map.off("moveend", state.moveendCallback);
this.map.off("draw.snap.options_changed", state.optionsChangedCallback);
var userPolygon = state.polygon;
if (state.options.overlap) {
DrawPolygon.onStop.call(this, state);
return;
}
// if overlap is false, mutate polygon so it doesn't overlap with existing ones
// get all editable features to check for intersections
var features = this._ctx.store.getAll();
try {
var edited = userPolygon;
features.forEach(function (feature) {
if (userPolygon.id === feature.id) return false;
if (!booleanIntersects(feature, edited)) return;
edited = turf.difference(edited, feature);
});
state.polygon.coordinates =
edited.coordinates || edited.geometry.coordinates;
} catch (err) {
// cancel this polygon if a difference cannot be calculated
DrawPolygon.onStop.call(this, state);
this.deleteFeature([state.polygon.id], { silent: true });
return;
}
// monkeypatch so DrawPolygon.onStop doesn't error
var rc = state.polygon.removeCoordinate;
state.polygon.removeCoordinate = () => {};
// This relies on the the state of SnapPolygonMode being similar to DrawPolygon
DrawPolygon.onStop.call(this, state);
state.polygon.removeCoordinate = rc.bind(state.polygon);
};
export default SnapPolygonMode;