-
-
Save chriswhong/694779bc1f1e5d926e47bab7205fa559 to your computer and use it in GitHub Desktop.
| // custom mapbopx-gl-draw mode that modifies draw_line_string | |
| // shows a center point, radius line, and circle polygon while drawing | |
| // forces draw.create on creation of second vertex | |
| import MapboxDraw from 'mapbox-gl-draw'; | |
| import numeral from 'numeral'; | |
| import lineDistance from 'npm:@turf/line-distance'; | |
| const RadiusMode = MapboxDraw.modes.draw_line_string; | |
| function createVertex(parentId, coordinates, path, selected) { | |
| return { | |
| type: 'Feature', | |
| properties: { | |
| meta: 'vertex', | |
| parent: parentId, | |
| coord_path: path, | |
| active: (selected) ? 'true' : 'false', | |
| }, | |
| geometry: { | |
| type: 'Point', | |
| coordinates, | |
| }, | |
| }; | |
| } | |
| // create a circle-like polygon given a center point and radius | |
| // https://stackoverflow.com/questions/37599561/drawing-a-circle-with-the-radius-in-miles-meters-with-mapbox-gl-js/39006388#39006388 | |
| function createGeoJSONCircle(center, radiusInKm, parentId, points = 64) { | |
| const coords = { | |
| latitude: center[1], | |
| longitude: center[0], | |
| }; | |
| const km = radiusInKm; | |
| const ret = []; | |
| const distanceX = km / (111.320 * Math.cos((coords.latitude * Math.PI) / 180)); | |
| const distanceY = km / 110.574; | |
| let theta; | |
| let x; | |
| let y; | |
| for (let i = 0; i < points; i += 1) { | |
| theta = (i / points) * (2 * Math.PI); | |
| x = distanceX * Math.cos(theta); | |
| y = distanceY * Math.sin(theta); | |
| ret.push([coords.longitude + x, coords.latitude + y]); | |
| } | |
| ret.push(ret[0]); | |
| return { | |
| type: 'Feature', | |
| geometry: { | |
| type: 'Polygon', | |
| coordinates: [ret], | |
| }, | |
| properties: { | |
| parent: parentId, | |
| }, | |
| }; | |
| } | |
| function getDisplayMeasurements(feature) { | |
| // should log both metric and standard display strings for the current drawn feature | |
| // metric calculation | |
| const drawnLength = (lineDistance(feature) * 1000); // meters | |
| let metricUnits = 'm'; | |
| let metricFormat = '0,0'; | |
| let metricMeasurement; | |
| let standardUnits = 'feet'; | |
| let standardFormat = '0,0'; | |
| let standardMeasurement; | |
| metricMeasurement = drawnLength; | |
| if (drawnLength >= 1000) { // if over 1000 meters, upgrade metric | |
| metricMeasurement = drawnLength / 1000; | |
| metricUnits = 'km'; | |
| metricFormat = '0.00'; | |
| } | |
| standardMeasurement = drawnLength * 3.28084; | |
| if (standardMeasurement >= 5280) { // if over 5280 feet, upgrade standard | |
| standardMeasurement /= 5280; | |
| standardUnits = 'mi'; | |
| standardFormat = '0.00'; | |
| } | |
| const displayMeasurements = { | |
| metric: `${numeral(metricMeasurement).format(metricFormat)} ${metricUnits}`, | |
| standard: `${numeral(standardMeasurement).format(standardFormat)} ${standardUnits}`, | |
| }; | |
| return displayMeasurements; | |
| } | |
| const doubleClickZoom = { | |
| enable: (ctx) => { | |
| setTimeout(() => { | |
| // First check we've got a map and some context. | |
| if (!ctx.map || !ctx.map.doubleClickZoom || !ctx._ctx || !ctx._ctx.store || !ctx._ctx.store.getInitialConfigValue) return; | |
| // Now check initial state wasn't false (we leave it disabled if so) | |
| if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return; | |
| ctx.map.doubleClickZoom.enable(); | |
| }, 0); | |
| }, | |
| }; | |
| RadiusMode.clickAnywhere = function(state, e) { | |
| // this ends the drawing after the user creates a second point, triggering this.onStop | |
| if (state.currentVertexPosition === 1) { | |
| state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); | |
| return this.changeMode('simple_select', { featureIds: [state.line.id] }); | |
| } | |
| this.updateUIClasses({ mouse: 'add' }); | |
| state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); | |
| if (state.direction === 'forward') { | |
| state.currentVertexPosition += 1; // eslint-disable-line | |
| state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); | |
| } else { | |
| state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); | |
| } | |
| return null; | |
| }; | |
| // creates the final geojson point feature with a radius property | |
| // triggers draw.create | |
| RadiusMode.onStop = function(state) { | |
| doubleClickZoom.enable(this); | |
| this.activateUIButton(); | |
| // check to see if we've deleted this feature | |
| if (this.getFeature(state.line.id) === undefined) return; | |
| // remove last added coordinate | |
| state.line.removeCoordinate('0'); | |
| if (state.line.isValid()) { | |
| const lineGeoJson = state.line.toGeoJSON(); | |
| // reconfigure the geojson line into a geojson point with a radius property | |
| const pointWithRadius = { | |
| type: 'Feature', | |
| geometry: { | |
| type: 'Point', | |
| coordinates: lineGeoJson.geometry.coordinates[0], | |
| }, | |
| properties: { | |
| radius: (lineDistance(lineGeoJson) * 1000).toFixed(1), | |
| }, | |
| }; | |
| this.map.fire('draw.create', { | |
| features: [pointWithRadius], | |
| }); | |
| } else { | |
| this.deleteFeature([state.line.id], { silent: true }); | |
| this.changeMode('simple_select', {}, { silent: true }); | |
| } | |
| }; | |
| RadiusMode.toDisplayFeatures = function(state, geojson, display) { | |
| const isActiveLine = geojson.properties.id === state.line.id; | |
| geojson.properties.active = (isActiveLine) ? 'true' : 'false'; | |
| if (!isActiveLine) return display(geojson); | |
| // Only render the line if it has at least one real coordinate | |
| if (geojson.geometry.coordinates.length < 2) return null; | |
| geojson.properties.meta = 'feature'; | |
| // displays center vertex as a point feature | |
| display(createVertex( | |
| state.line.id, | |
| geojson.geometry.coordinates[state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1], | |
| `${state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1}`, | |
| false, | |
| )); | |
| // displays the line as it is drawn | |
| display(geojson); | |
| const displayMeasurements = getDisplayMeasurements(geojson); | |
| // create custom feature for the current pointer position | |
| const currentVertex = { | |
| type: 'Feature', | |
| properties: { | |
| meta: 'currentPosition', | |
| radiusMetric: displayMeasurements.metric, | |
| radiusStandard: displayMeasurements.standard, | |
| parent: state.line.id, | |
| }, | |
| geometry: { | |
| type: 'Point', | |
| coordinates: geojson.geometry.coordinates[1], | |
| }, | |
| }; | |
| display(currentVertex); | |
| // create custom feature for radius circlemarker | |
| const center = geojson.geometry.coordinates[0]; | |
| const radiusInKm = lineDistance(geojson, 'kilometers'); | |
| const circleFeature = createGeoJSONCircle(center, radiusInKm, state.line.id); | |
| circleFeature.properties.meta = 'radius'; | |
| display(circleFeature); | |
| return null; | |
| }; | |
| export default RadiusMode; |
I've published an npm module called mapbox-gl-draw-circle which lets you draw and edit a circle. Please do take a look and feel free to reach out to me if you find any issues.
I've published an npm module called mapbox-gl-draw-circle which lets you draw and edit a circle. Please do take a look and feel free to reach out to me if you find any issues.
Project has been dropped, not updated in 4 years :(
ufff, there is no documentation of all those function
toDisplayFeatures,clickAnywhere,state: active.Notice if you move the circle around it changes to oval (ellipse), depends upon the radius if u draw a large circle around north pole, and then move around it changes to oval
I think "clickAnyWhere" is "onClick"
https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/MODES.md#modeonclick
And I make typescript version.
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import numeral from "numeral";
import lineDistance from "@turf/line-distance";
import { GeoJSON } from "geojson";
const DrawLine = MapboxDraw.modes.draw_line_string;
const RadiusMode = { ...DrawLine };
const createVertex = (
parentId: string,
coordinates: [number, number],
path: string,
selected: boolean
) => {
return {
type: "Feature",
properties: {
meta: "vertex",
parent: parentId,
coord_path: path,
active: selected ? "true" : "false",
},
geometry: {
type: "Point",
coordinates,
},
} as GeoJSON;
};
const createGeoJSONCircle = (
center: [number, number],
radiusInKm: number,
parentId: string,
points: number = 64
) => {
const coords = {
latitude: center[1],
longitude: center[0],
};
const km = radiusInKm;
const ret = [];
const distanceX = km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180));
const distanceY = km / 110.574;
let theta;
let x;
let y;
for (let i = 0; i < points; i += 1) {
theta = (i / points) * (2 * Math.PI);
x = distanceX * Math.cos(theta);
y = distanceY * Math.sin(theta);
ret.push([coords.longitude + x, coords.latitude + y]);
}
ret.push([ret[0]]);
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [ret],
},
properties: {
parent: parentId,
},
};
};
const getDisplayMeasurements = (feature: GeoJSON.Feature) => {
const drawLength = lineDistance(feature) * 1000;
let metricUnits = "m";
let metricFormat = "0,0";
let metricMeasurement;
let standardUnits = "feet";
let standardFormat = "0,0";
let standardMeasurement;
metricMeasurement = drawLength;
if (drawLength >= 1000) {
metricMeasurement = drawLength / 1000;
metricUnits = "km";
metricFormat = "0.00";
}
standardMeasurement = drawLength * 3.28084;
if (standardMeasurement >= 5280) {
standardMeasurement /= 5280;
standardUnits = "mi";
standardFormat = "0.00";
}
const displayMeasurements = {
metric: `${numeral(metricMeasurement).format(metricFormat)} ${metricUnits}`,
standard: `${numeral(standardMeasurement).format(
standardFormat
)} ${standardUnits}`,
};
return displayMeasurements;
};
const doubleClickZoom = {
enable: (ctx: any) => {
setTimeout(() => {
if (
!ctx.map ||
!ctx.map.doubleClickZoom ||
!ctx._ctx ||
!ctx._ctx.store ||
!ctx._ctx.store.getInitialConfigValue
)
return;
if (!ctx._ctx.store.getInitialConfigValue("doubleClickZoom")) return;
ctx.map.doubleClickXoom.enabled();
}, 0);
},
};
RadiusMode.onClick = function (state: any, e: any) {
// this ends the drawing after the user creates a second point, triggering this.onStop
if (state.currentVertexPosition === 1) {
state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat);
return this.changeMode("simple_select", { featureIds: [state.line.id] });
}
this.updateUIClasses({ mouse: "add" });
state.line.updateCoordinate(
state.currentVertexPosition,
e.lngLat.lng,
e.lngLat.lat
);
if (state.direction === "forward") {
state.currentVertexPosition += 1; // eslint-disable-line
state.line.updateCoordinate(
state.currentVertexPosition,
e.lngLat.lng,
e.lngLat.lat
);
} else {
state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat);
}
return null;
};
// creates the final geojson point feature with a radius property
// triggers draw.create
RadiusMode.onStop = function (state) {
doubleClickZoom.enable(this);
this.activateUIButton();
// check to see if we've deleted this feature
if (this.getFeature(state.line.id) === undefined) return;
// remove last added coordinate
state.line.removeCoordinate("0");
if (state.line.isValid()) {
const lineGeoJson = state.line.toGeoJSON();
// reconfigure the geojson line into a geojson point with a radius property
const pointWithRadius = {
type: "Feature",
geometry: {
type: "Point",
coordinates: lineGeoJson.geometry.coordinates[0],
},
properties: {
radius: (lineDistance(lineGeoJson) * 1000).toFixed(1),
},
};
this.map.fire("draw.create", {
features: [pointWithRadius],
});
} else {
this.deleteFeature(state.line.id, { silent: true });
// this.deleteFeature([state.line.id], { silent: true });
this.changeMode("simple_select", {}, { silent: true });
}
};
RadiusMode.toDisplayFeatures = function (state, geojson: any, display) {
const isActiveLine = geojson.properties.id === state.line.id;
geojson.properties.active = isActiveLine ? "true" : "false";
if (!isActiveLine) return display(geojson);
// Only render the line if it has at least one real coordinate
if (geojson.geometry.coordinates.length < 2) return null;
geojson.properties.meta = "feature";
// displays center vertex as a point feature
display(
createVertex(
state.line.id,
geojson.geometry.coordinates[
state.direction === "forward"
? geojson.geometry.coordinates.length - 2
: 1
],
`${
state.direction === "forward"
? geojson.geometry.coordinates.length - 2
: 1
}`,
false
)
);
// displays the line as it is drawn
display(geojson);
const displayMeasurements = getDisplayMeasurements(geojson);
// create custom feature for the current pointer position
const currentVertex = {
type: "Feature",
properties: {
meta: "currentPosition",
radiusMetric: displayMeasurements.metric,
radiusStandard: displayMeasurements.standard,
parent: state.line.id,
},
geometry: {
type: "Point",
coordinates: geojson.geometry.coordinates[1],
},
} as GeoJSON;
display(currentVertex);
// create custom feature for radius circlemarker
const center = geojson.geometry.coordinates[0];
const radiusInKm = lineDistance(geojson, "kilometers");
const circleFeaturee = createGeoJSONCircle(center, radiusInKm, state.line.id);
const circleFeature = {
...circleFeaturee,
properties: { ...circleFeaturee.properties, meta: "radius" },
} as GeoJSON;
display(circleFeature);
return null;
};
export default RadiusMode;
const modes = {
...MapboxDraw.modes,
draw_radius: RadiusMode,
},
const Draw = new MapboxDraw({
defaultMode: "draw_radius",
displayControlsDefault: false,
userProperties: true,
modes,
});
@gtsl-2 I got errors with your version : it was consuming so much memory that my app was crashing.. any idea why ?
/**
* Custom mapbox-gl-draw mode that modifies draw_line_string.
* - Shows a center point, radius line, and circle polygon while drawing.
* - Forces draw.create on creation of second vertex.
*
* Sourced from: https://github.com/NYCPlanning/labs-factfinder/blob/63bdff401c0a00dd158286d21c165ca885bc02fd/app/utils/radius-mode.js
*/
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import lineDistance from '@turf/line-distance';
import type { Feature, GeoJSON, GeoJsonProperties, LineString, Point, Polygon } from 'geojson';
import type { MapMouseEvent } from 'mapbox-gl';
import numeral from 'numeral';
// --- TYPE DEFINITIONS ---
// The internal state for the RadiusMode.
interface RadiusModeState {
line: MapboxDraw.DrawLineString;
currentVertexPosition: number;
direction: 'forward' | 'backward';
}
// Define the structure for the display measurements object.
interface DisplayMeasurements {
feet: string;
miles: string;
}
// Mapbox Draw's `this` context for custom modes.
type DrawModeContext = MapboxDraw.DrawCustomModeThis;
// --- UTILITY FUNCTIONS ---
/**
* Creates a GeoJSON Point Feature used to represent a vertex.
* @param parentId - The ID of the parent feature.
* @param coordinates - The coordinates of the vertex.
* @param path - The coordinate path of the vertex.
* @param selected - Whether the vertex is selected.
* @returns A GeoJSON Point Feature for the vertex.
*/
function createVertex(parentId: string, coordinates: number[], path: string, selected: boolean): Feature<Point> {
return {
type: 'Feature',
properties: {
meta: 'vertex',
parent: parentId,
coord_path: path,
active: selected ? 'true' : 'false',
},
geometry: {
type: 'Point',
coordinates,
},
};
}
/**
* Creates a circle-like polygon given a center point and a radius.
* @param center - The center coordinates [lng, lat].
* @param radiusInKm - The radius of the circle in kilometers.
* @param parentId - The ID of the parent feature.
* @param points - The number of points to use to approximate the circle.
* @returns A GeoJSON Polygon Feature representing the circle.
*/
function createGeoJSONCircle(center: number[], radiusInKm: number, parentId: string, points = 64): Feature<Polygon> {
const coords = {
latitude: center[1],
longitude: center[0],
};
const km = radiusInKm;
const ret: number[][] = [];
const distanceX = km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180));
const distanceY = km / 110.574;
let theta: number;
let x: number;
let y: number;
for (let i = 0; i < points; i += 1) {
theta = (i / points) * (2 * Math.PI);
x = distanceX * Math.cos(theta);
y = distanceY * Math.sin(theta);
ret.push([coords.longitude + x, coords.latitude + y]);
}
ret.push(ret[0]);
return {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [ret],
},
properties: {
parent: parentId,
},
};
}
/**
* Creates metric and standard display strings for the line's length.
* @param lineFeature - The GeoJSON LineString feature to measure.
* @returns An object with formatted length strings.
*/
function getDisplayMeasurements(lineFeature: Feature<LineString>): DisplayMeasurements {
const drawnLength = lineDistance(lineFeature) * 1000; // in meters
const feetMeasurement = drawnLength * 3.28084;
const milesMeasurement = feetMeasurement / 5280;
return {
feet: `${numeral(feetMeasurement).format('0,0')} ft`,
miles: `${numeral(milesMeasurement).format('0.00')} mi`,
};
}
// Helper to re-enable double-click zoom.
const doubleClickZoom = {
enable: (ctx: DrawModeContext) => {
setTimeout(() => {
if (
!ctx.map ||
!ctx.map.doubleClickZoom
)
return;
if (!ctx.map.doubleClickZoom) return;
ctx.map.doubleClickZoom.enable();
}, 0);
},
};
// --- CUSTOM MODE DEFINITION ---
const RadiusMode: any = { ...MapboxDraw.modes.draw_line_string };
RadiusMode.clickAnywhere = function (this: DrawModeContext, state: RadiusModeState, e: MapMouseEvent) {
// End drawing after the user creates a second point.
if (state.currentVertexPosition === 1) {
state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat);
return this.changeMode('simple_select', { featureIds: [state.line.id] });
}
this.updateUIClasses({ mouse: 'add' });
state.line.updateCoordinate(state.currentVertexPosition.toString(), e.lngLat.lng, e.lngLat.lat);
if (state.direction === 'forward') {
state.currentVertexPosition += 1;
state.line.updateCoordinate(state.currentVertexPosition.toString(), e.lngLat.lng, e.lngLat.lat);
} else {
state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat);
}
return null;
};
RadiusMode.onStop = function (this: DrawModeContext, state: RadiusModeState) {
doubleClickZoom.enable(this);
this.activateUIButton();
if (this.getFeature(state.line.id as string) === undefined) return;
// Clean up the feature by removing the temporary second point.
state.line.removeCoordinate('0');
if (state.line.isValid()) {
const lineGeoJson = state.line.toGeoJSON() as GeoJSON.Feature<LineString, GeoJsonProperties>;
// Reconfigure the GeoJSON line into a GeoJSON point with a radius property.
const pointWithRadius: Feature<Point> = {
type: 'Feature',
geometry: {
type: 'Point',
// The first coordinate is the center.
coordinates: lineGeoJson.geometry.coordinates[0],
},
properties: {
radius: (lineDistance(lineGeoJson) * 1000).toFixed(1), // radius in meters
},
};
this.map.fire('draw.create', {
features: [pointWithRadius],
});
} else {
this.deleteFeature(state.line.id as any, { silent: true });
this.changeMode('simple_select', {}, { silent: true });
}
};
RadiusMode.toDisplayFeatures = function (
this: DrawModeContext,
state: RadiusModeState,
geojson: Feature,
display: (geojson: GeoJSON) => void,
) {
const isActiveLine = geojson.properties?.id === state.line.id;
if (geojson.properties) {
geojson.properties.active = isActiveLine ? 'true' : 'false';
}
if (!isActiveLine) return display(geojson);
// Do not render if there are not enough coordinates
if (!geojson.geometry || geojson.geometry.type !== 'LineString' || geojson.geometry.coordinates.length < 2) return null;
if (geojson.properties) {
geojson.properties.meta = 'feature';
}
// Display the center vertex
const centerCoordIndex = state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1;
display(
createVertex(
state.line.id as string,
geojson.geometry.coordinates[centerCoordIndex],
`${centerCoordIndex}`,
false,
),
);
// Display the radius line as it is being drawn
display(geojson);
// Get and display measurements
const displayMeasurements = getDisplayMeasurements(geojson as Feature<LineString>);
// Create a custom feature for the current pointer position with measurement info
const currentVertex: Feature<Point> = {
type: 'Feature',
properties: {
meta: 'currentPosition',
radiusFeet: displayMeasurements.feet,
radiusMiles: displayMeasurements.miles,
parent: state.line.id,
},
geometry: {
type: 'Point',
// The second coordinate is the live pointer position.
coordinates: geojson.geometry.coordinates[1],
},
};
display(currentVertex);
// Create and display the circle polygon
const center = geojson.geometry.coordinates[0];
const radiusInKm = lineDistance(geojson as Feature<LineString>, 'kilometers');
const circleFeature = createGeoJSONCircle(center, radiusInKm, state.line.id as string);
if (circleFeature.properties) {
circleFeature.properties.meta = 'radius';
}
display(circleFeature);
return null;
};
export default RadiusMode;Here's an updated version (few any because the state.line.id isn't at the right type) but it works well.
ufff,
there is no documentation of all those function
toDisplayFeatures,clickAnywhere,state: active.Notice if you move the circle around it changes to oval (ellipse), depends upon the radius if u draw a large circle around north pole, and then move around it changes to oval