Skip to content

Instantly share code, notes, and snippets.

@chriswhong
Created March 1, 2018 12:04
Show Gist options
  • Save chriswhong/694779bc1f1e5d926e47bab7205fa559 to your computer and use it in GitHub Desktop.
Save chriswhong/694779bc1f1e5d926e47bab7205fa559 to your computer and use it in GitHub Desktop.
RadiusMode, a custom mode for mapbox-gl-draw for drawing a radius
// 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;
@behuda
Copy link

behuda commented Mar 26, 2019

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

@iamanvesh
Copy link

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.

@molinto
Copy link

molinto commented Apr 18, 2023

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 :(

@gtsl-2
Copy link

gtsl-2 commented Apr 11, 2025

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,
      });
      

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment