Instantly share code, notes, and snippets.

Embed
What would you like to do?
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;
@goors

This comment has been minimized.

Show comment
Hide comment
@goors

goors Jun 22, 2018

How do you keep circle after draw? Only thing you get is line ... ?

goors commented Jun 22, 2018

How do you keep circle after draw? Only thing you get is line ... ?

@rcowling

This comment has been minimized.

Show comment
Hide comment
@rcowling

rcowling Jun 26, 2018

is there an example of this being used in code? How do you implement it?

rcowling commented Jun 26, 2018

is there an example of this being used in code? How do you implement it?

@thu07

This comment has been minimized.

Show comment
Hide comment
@thu07

thu07 Jun 28, 2018

Thanks for your great job. I met a problem when I add your code as a new mode. On the map, it just shows the line, no circle. Do you know why? By the way, I changed the 'turf/line-distance' to the latest library named 'turf/length'. Look forward to your reply. Thank you so much.

thu07 commented Jun 28, 2018

Thanks for your great job. I met a problem when I add your code as a new mode. On the map, it just shows the line, no circle. Do you know why? By the way, I changed the 'turf/line-distance' to the latest library named 'turf/length'. Look forward to your reply. Thank you so much.

@mikeomeara1

This comment has been minimized.

Show comment
Hide comment
@mikeomeara1

mikeomeara1 Jun 28, 2018

@thu07 would you mind positing your upgraded turf code. I noticed this too about linedistance being depreciated but you beat me to it.

I'm also running up against the same issue where we get a line and that's it. If anyone has any clues, I'm all ears.

mikeomeara1 commented Jun 28, 2018

@thu07 would you mind positing your upgraded turf code. I noticed this too about linedistance being depreciated but you beat me to it.

I'm also running up against the same issue where we get a line and that's it. If anyone has any clues, I'm all ears.

@thu07

This comment has been minimized.

Show comment
Hide comment
@thu07

thu07 Jun 28, 2018

@mikeomeara1 Thank you for the response. I finally find out the problem. In the function createGeoJSONCircle(), you should add 'active:'true'' in the property and it works. Please check it.

thu07 commented Jun 28, 2018

@mikeomeara1 Thank you for the response. I finally find out the problem. In the function createGeoJSONCircle(), you should add 'active:'true'' in the property and it works. Please check it.

@rcowling

This comment has been minimized.

Show comment
Hide comment
@rcowling

rcowling Jun 29, 2018

@thu07 do you get the circle to stay on the screen or is it just a line? Can you post the more of the part of your code to add that please?

rcowling commented Jun 29, 2018

@thu07 do you get the circle to stay on the screen or is it just a line? Can you post the more of the part of your code to add that please?

@thu07

This comment has been minimized.

Show comment
Hide comment
@thu07

thu07 Jun 29, 2018

@rcowling I didn't get the circle to stay on the screen. The circle is shown on the map when I draw it. That's what I want. Of course, if you want to do something else, you have to modify the codes if necessary.

thu07 commented Jun 29, 2018

@rcowling I didn't get the circle to stay on the screen. The circle is shown on the map when I draw it. That's what I want. Of course, if you want to do something else, you have to modify the codes if necessary.

@ddebarros

This comment has been minimized.

Show comment
Hide comment
@ddebarros

ddebarros Jul 2, 2018

@rcowling to get the circle to stay, in the RadiusMode.onStop add pointWithRadius = createGeoJSONCircle(lineGeoJson.geometry.coordinates[0],(lineDistance(lineGeoJson)).toFixed(1), null, 20)
this worked for me.

ddebarros commented Jul 2, 2018

@rcowling to get the circle to stay, in the RadiusMode.onStop add pointWithRadius = createGeoJSONCircle(lineGeoJson.geometry.coordinates[0],(lineDistance(lineGeoJson)).toFixed(1), null, 20)
this worked for me.

@IThordGray

This comment has been minimized.

Show comment
Hide comment
@IThordGray

IThordGray Jul 5, 2018

@ddebarros Was there anything else you changed? I've tried your suggestion, and with breakpoints I can see the polygon data is correct(compared to an actual polygon draw), but it still renders only a line afterwards.

My code snippet from RadiusMode.onStop

  if (state.line.isValid()) {
    const lineGeoJson = state.line.toGeoJSON();
    
    const pointWithRadius = createGeoJSONCircle(lineGeoJson.geometry.coordinates[0], (length(lineGeoJson) * 1000), null, 20);

    this.map.fire('draw.create', {
      features: [pointWithRadius],
    });
  } else {
    this.deleteFeature([state.line.id], {silent: true});
    this.changeMode('simple_select', {}, {silent: true});
  }

As you'll also notice, I'm using length instead of lineDistance, but it all seems fine.

Any other suggestions are also welcome :)

IThordGray commented Jul 5, 2018

@ddebarros Was there anything else you changed? I've tried your suggestion, and with breakpoints I can see the polygon data is correct(compared to an actual polygon draw), but it still renders only a line afterwards.

My code snippet from RadiusMode.onStop

  if (state.line.isValid()) {
    const lineGeoJson = state.line.toGeoJSON();
    
    const pointWithRadius = createGeoJSONCircle(lineGeoJson.geometry.coordinates[0], (length(lineGeoJson) * 1000), null, 20);

    this.map.fire('draw.create', {
      features: [pointWithRadius],
    });
  } else {
    this.deleteFeature([state.line.id], {silent: true});
    this.changeMode('simple_select', {}, {silent: true});
  }

As you'll also notice, I'm using length instead of lineDistance, but it all seems fine.

Any other suggestions are also welcome :)

@ddebarros

This comment has been minimized.

Show comment
Hide comment
@ddebarros

ddebarros Jul 8, 2018

@IThordGray in createGeoJSONCircle after line 60 be sure to set add active: 'true',
same in currentVertex after line 192

ddebarros commented Jul 8, 2018

@IThordGray in createGeoJSONCircle after line 60 be sure to set add active: 'true',
same in currentVertex after line 192

@JAugustusSmith

This comment has been minimized.

Show comment
Hide comment
@JAugustusSmith

JAugustusSmith Jul 9, 2018

Is there a working example of this being used?
I'm trying to modify this to show radius when resizing, and area when completed

JAugustusSmith commented Jul 9, 2018

Is there a working example of this being used?
I'm trying to modify this to show radius when resizing, and area when completed

@KayBeSee

This comment has been minimized.

Show comment
Hide comment
@KayBeSee

KayBeSee Jul 31, 2018

@JAugustusSmith Here is an example: https://popfactfinder.planning.nyc.gov/

Does anyone have an example of instantiating a Mapbox Draw with this mode? I am getting errors with functions called by this (i.e. this.changeMode('simple_select', {}, { silent: true });)

KayBeSee commented Jul 31, 2018

@JAugustusSmith Here is an example: https://popfactfinder.planning.nyc.gov/

Does anyone have an example of instantiating a Mapbox Draw with this mode? I am getting errors with functions called by this (i.e. this.changeMode('simple_select', {}, { silent: true });)

@reeversedev

This comment has been minimized.

Show comment
Hide comment
@reeversedev

reeversedev Aug 8, 2018

Hi Guys, I am currently trying to Implement the draw feature to draw a circle on the map. But I am unable to use it. Can anyone please help with the use case and the implementation?

reeversedev commented Aug 8, 2018

Hi Guys, I am currently trying to Implement the draw feature to draw a circle on the map. But I am unable to use it. Can anyone please help with the use case and the implementation?

@chriswhong

This comment has been minimized.

Show comment
Hide comment
@chriswhong

chriswhong Aug 8, 2018

Hello all, I had no idea all these comments were here, sorry for not responding. You can see this radius code in action here, just choose the draw tools and you'll see it.

https://popfactfinder.planning.nyc.gov/#12.25/40.724/-73.9868

The actual place it is implemented in the code is here: https://github.com/NYCPlanning/labs-factfinder/blob/63bdff401c0a00dd158286d21c165ca885bc02fd/app/utils/radius-mode.js

Owner

chriswhong commented Aug 8, 2018

Hello all, I had no idea all these comments were here, sorry for not responding. You can see this radius code in action here, just choose the draw tools and you'll see it.

https://popfactfinder.planning.nyc.gov/#12.25/40.724/-73.9868

The actual place it is implemented in the code is here: https://github.com/NYCPlanning/labs-factfinder/blob/63bdff401c0a00dd158286d21c165ca885bc02fd/app/utils/radius-mode.js

@benderlidze

This comment has been minimized.

Show comment
Hide comment
@benderlidze

benderlidze Aug 11, 2018

Well, we all need some jsfiddle example that will leave the circle on the map and the ability to interact with it(edit, move, delete so on).
here is a fiddle that will leave the circle(new source and layer) but with no further interaction, any ideas how to add this?
https://jsfiddle.net/ajyo8w9c/1/

and how to leave the circle on the map with mapbox-gl-draw api?
as I understand I have to save the line in circle props for future editing, but what to do next?

benderlidze commented Aug 11, 2018

Well, we all need some jsfiddle example that will leave the circle on the map and the ability to interact with it(edit, move, delete so on).
here is a fiddle that will leave the circle(new source and layer) but with no further interaction, any ideas how to add this?
https://jsfiddle.net/ajyo8w9c/1/

and how to leave the circle on the map with mapbox-gl-draw api?
as I understand I have to save the line in circle props for future editing, but what to do next?

@fc

This comment has been minimized.

Show comment
Hide comment
@fc

fc Sep 12, 2018

This adds the circle. It's not possible to change the radius once the circle has been added.

// Radius mode
// Source:
// https://gist.github.com/chriswhong/694779bc1f1e5d926e47bab7205fa559
// 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
/* eslint-disable no-underscore-dangle */
/* eslint-disable react/no-this-in-sfc */
/* eslint-disable func-names */
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import numeral from "numeral";
import _ from "lodash";
import lineDistance from "@turf/length";

const RadiusMode = _.extend({}, 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.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
    }
  };
}

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.onSetup = function(opts) {
  const props = MapboxDraw.modes.draw_line_string.onSetup.call(this, opts);
  const circle = this.newFeature({
    type: "Feature",
    properties: {
      meta: "radius"
    },
    geometry: {
      type: "Polygon",
      coordinates: [[]]
    }
  });
  this.addFeature(circle);

  return {
    ...props,
    circle
  };
};

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

RadiusMode.onMouseMove = function(state, e) {
  MapboxDraw.modes.draw_line_string.onMouseMove.call(this, state, e);
  const geojson = state.line.toGeoJSON();
  const center = geojson.geometry.coordinates[0];
  const radiusInKm = lineDistance(geojson, "kilometers");
  const circleFeature = createGeoJSONCircle(center, radiusInKm, state.line.id);
  circleFeature.properties.meta = "radius";
  state.circle.setCoordinates(circleFeature.geometry.coordinates);
};

// 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 geojson = state.line.toGeoJSON();
    this.deleteFeature([state.line.id], { silent: true });

    this.map.fire("draw.create", {
      features: [state.circle.toGeoJSON()]
    });
  } 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);

  return null;
};

export default RadiusMode;

If you want to have the cool info layer as shown in @chriswhong's NYC example, checkout the styles used here:
https://github.com/NYCPlanning/labs-factfinder/blob/63bdff401c0a00dd158286d21c165ca885bc02fd/app/layers/draw-styles.js#L69-L100

An alternative extremely limited (crippled) solution but is maybe helpful for someone... creating a wrapper around mapbox-gl-circle which does allow changing the radius (this solution doesn't really play well with mapbox-gl-draw), or possibly better is to use mapbox-gl-circle directly by using the onClick event from the map itself:

import MapboxCircle from "mapbox-gl-circle";

const CircleMode = {
  onSetup(opts) {
    this.updateUIClasses({ mouse: "add" });
    return {};
  },
  onClick(state, e) {
    const circle = new MapboxCircle(e.lngLat, 1000, {
      editable: true,
      minRadius: 5,
      fillColor: "#29AB87"
    }).addTo(this.map);
    this.changeMode("simple_select");
    // console.log(circle);
  }
};

fc commented Sep 12, 2018

This adds the circle. It's not possible to change the radius once the circle has been added.

// Radius mode
// Source:
// https://gist.github.com/chriswhong/694779bc1f1e5d926e47bab7205fa559
// 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
/* eslint-disable no-underscore-dangle */
/* eslint-disable react/no-this-in-sfc */
/* eslint-disable func-names */
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import numeral from "numeral";
import _ from "lodash";
import lineDistance from "@turf/length";

const RadiusMode = _.extend({}, 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.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
    }
  };
}

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.onSetup = function(opts) {
  const props = MapboxDraw.modes.draw_line_string.onSetup.call(this, opts);
  const circle = this.newFeature({
    type: "Feature",
    properties: {
      meta: "radius"
    },
    geometry: {
      type: "Polygon",
      coordinates: [[]]
    }
  });
  this.addFeature(circle);

  return {
    ...props,
    circle
  };
};

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

RadiusMode.onMouseMove = function(state, e) {
  MapboxDraw.modes.draw_line_string.onMouseMove.call(this, state, e);
  const geojson = state.line.toGeoJSON();
  const center = geojson.geometry.coordinates[0];
  const radiusInKm = lineDistance(geojson, "kilometers");
  const circleFeature = createGeoJSONCircle(center, radiusInKm, state.line.id);
  circleFeature.properties.meta = "radius";
  state.circle.setCoordinates(circleFeature.geometry.coordinates);
};

// 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 geojson = state.line.toGeoJSON();
    this.deleteFeature([state.line.id], { silent: true });

    this.map.fire("draw.create", {
      features: [state.circle.toGeoJSON()]
    });
  } 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);

  return null;
};

export default RadiusMode;

If you want to have the cool info layer as shown in @chriswhong's NYC example, checkout the styles used here:
https://github.com/NYCPlanning/labs-factfinder/blob/63bdff401c0a00dd158286d21c165ca885bc02fd/app/layers/draw-styles.js#L69-L100

An alternative extremely limited (crippled) solution but is maybe helpful for someone... creating a wrapper around mapbox-gl-circle which does allow changing the radius (this solution doesn't really play well with mapbox-gl-draw), or possibly better is to use mapbox-gl-circle directly by using the onClick event from the map itself:

import MapboxCircle from "mapbox-gl-circle";

const CircleMode = {
  onSetup(opts) {
    this.updateUIClasses({ mouse: "add" });
    return {};
  },
  onClick(state, e) {
    const circle = new MapboxCircle(e.lngLat, 1000, {
      editable: true,
      minRadius: 5,
      fillColor: "#29AB87"
    }).addTo(this.map);
    this.changeMode("simple_select");
    // console.log(circle);
  }
};
@zhaozhe0831

This comment has been minimized.

Show comment
Hide comment
@zhaozhe0831

zhaozhe0831 Sep 20, 2018

@fc thank you for your code. But how to change circle radius when I draw a circle polygon?

zhaozhe0831 commented Sep 20, 2018

@fc thank you for your code. But how to change circle radius when I draw a circle polygon?

@fc

This comment has been minimized.

Show comment
Hide comment
@fc

fc Sep 20, 2018

@zhaozhe0831 I did not want to take on the work for that and instead used mapbox-gl-circle to achieve what I wanted.

If you or someone else wants to take on customizing the radius:

  • you might be able to achieve it by removing the second point of the line to have it revert to the previous drawing state
  • you'll likely want to override the onClick behavior (which is currently inherited from line mode) and check if a vertex is clicked on the circle, then re-create the state of the circle as if it is drawing a new one. That's what I'd explore doing.

Good luck.

fc commented Sep 20, 2018

@zhaozhe0831 I did not want to take on the work for that and instead used mapbox-gl-circle to achieve what I wanted.

If you or someone else wants to take on customizing the radius:

  • you might be able to achieve it by removing the second point of the line to have it revert to the previous drawing state
  • you'll likely want to override the onClick behavior (which is currently inherited from line mode) and check if a vertex is clicked on the circle, then re-create the state of the circle as if it is drawing a new one. That's what I'd explore doing.

Good luck.

@zhaozhe0831

This comment has been minimized.

Show comment
Hide comment
@zhaozhe0831

zhaozhe0831 Sep 21, 2018

@fc Thank you for your advice.

zhaozhe0831 commented Sep 21, 2018

@fc Thank you for your advice.

@zhaozhe0831

This comment has been minimized.

Show comment
Hide comment
@zhaozhe0831

zhaozhe0831 Sep 21, 2018

image
image
@fc I find a problem,and I can't resolve the problem。Do you know the reason?what info do you need?
image

zhaozhe0831 commented Sep 21, 2018

image
image
@fc I find a problem,and I can't resolve the problem。Do you know the reason?what info do you need?
image

@fc

This comment has been minimized.

Show comment
Hide comment
@fc

fc Sep 21, 2018

@zhaozhe0831 Is it a specific problem with the code I posted? I have just re-tested it and it locally and it seems fine. While it certainly needs work to achieve what you're looking for I'm not supporting it at all.

As to your error:
An error of "Cannot read property of null" likely means you have a variable that is null but the code is expecting the variable to be an array and it errors out when it sees it is null.

Below is how I was initializing it which is maybe helpful for someone although unlikely to be related to your error.

import React, { Component } from "react";
import "./App.css";
import ReactMapboxGl, { ZoomControl } from "react-mapbox-gl";
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import "mapbox-gl/dist/mapbox-gl.css";
import "@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css";
import RadiusMode from "./RadiusMode";

const Mapbox = ReactMapboxGl({
  accessToken:
    "add_your_token"
});

const mapboxProps = {
  style: "mapbox://styles/your_styles_goes_here",
  zoom: [8],
  containerStyle: {
    height: "100vh ",
    width: "100vw"
  }
};

const draw = new MapboxDraw({
  defaultMode: "draw_circle",
  modes: {
    ...MapboxDraw.modes,
    draw_circle: RadiusMode
  },
  userProperties: true,
  displayControlsDefault: false
});
draw.modes.DRAW_CIRCLE = "draw_circle";

class App extends Component {
  handleMapLoaded = map => {
    map.addControl(draw);
    map.fitBounds([
      [-0.12646452955218024, 51.47684019822054],
      [-0.07084624343019641, 51.50398878236996]
    ]);
  };
  render() {
    return (
      <div className="App">
        <Mapbox {...mapboxProps} onStyleLoad={this.handleMapLoaded}>
          <ZoomControl position="top-left" />
        </Mapbox>
      </div>
    );
  }
}

export default App;

fc commented Sep 21, 2018

@zhaozhe0831 Is it a specific problem with the code I posted? I have just re-tested it and it locally and it seems fine. While it certainly needs work to achieve what you're looking for I'm not supporting it at all.

As to your error:
An error of "Cannot read property of null" likely means you have a variable that is null but the code is expecting the variable to be an array and it errors out when it sees it is null.

Below is how I was initializing it which is maybe helpful for someone although unlikely to be related to your error.

import React, { Component } from "react";
import "./App.css";
import ReactMapboxGl, { ZoomControl } from "react-mapbox-gl";
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import "mapbox-gl/dist/mapbox-gl.css";
import "@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css";
import RadiusMode from "./RadiusMode";

const Mapbox = ReactMapboxGl({
  accessToken:
    "add_your_token"
});

const mapboxProps = {
  style: "mapbox://styles/your_styles_goes_here",
  zoom: [8],
  containerStyle: {
    height: "100vh ",
    width: "100vw"
  }
};

const draw = new MapboxDraw({
  defaultMode: "draw_circle",
  modes: {
    ...MapboxDraw.modes,
    draw_circle: RadiusMode
  },
  userProperties: true,
  displayControlsDefault: false
});
draw.modes.DRAW_CIRCLE = "draw_circle";

class App extends Component {
  handleMapLoaded = map => {
    map.addControl(draw);
    map.fitBounds([
      [-0.12646452955218024, 51.47684019822054],
      [-0.07084624343019641, 51.50398878236996]
    ]);
  };
  render() {
    return (
      <div className="App">
        <Mapbox {...mapboxProps} onStyleLoad={this.handleMapLoaded}>
          <ZoomControl position="top-left" />
        </Mapbox>
      </div>
    );
  }
}

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