Leaflet – Editable Polygon with Different Sized Two-Part Border

leafletleaflet-drawleaflet-pluginspolygon

I'm a beginner in Leaflet JS and I have a polygon drawn on the map:

var coords= [
   [27.40839959582344,-82.40601726602196],
   [27.40859871554781,-82.40638191331237],
   [27.40834938302612,-82.40652231229888]
];

L.polygon(coords, { fillColor: 'blue', fillOpacity: 0.5, weight: 75 / Math.pow(2, 19 - map.getZoom()), color: 'red', opacity: 0.5, fill: true}).addTo(map);

and I'm changing the border weight every time I change the zoom level, to keep it to about 20 meters

map.on('zoomend', function (e) {
   map.eachLayer(function (layer) {
      layer.setStyle({ weight: map.getZoom() > 16 ? (75 / Math.pow(2, 19 - map.getZoom())) : 15 });
      layer.bringToFront();
   });
}

JSFiddle

What is best to use in order to increase the outer part of the border, from 10 m to 15 m but to keep the inside part (that overlaps the fill color) the same size of 10? Can it work with drawControl when editing and deleting the polygon?

EDIT:
enter image description here

In the image the polygon filling is red and the border(stroke) blue. Half of the border is overlapping the filling (becoming a purplish color). That is the inner part of the polygon stroke that should remain in place/the same size. The other half (that has the default blue color – drawn on the outside) is the one that should increase in size to the zone marked with the pen. I need to increase the width only for half of the stroke

Best Answer

Since border dimensions are in map units, obvious candidate for custom border creation is turf.js library.

Basic principle to create polygon with custom inner and outer border, where each part of polygon (inner border, outer border, interior) has it's own style, would be:

  • Create polygon buffer with positive offset with turf.buffer method, then create difference between buffer and polygon with turf.difference method. Resulting polygon is outer border.
  • Create polygon buffer with negative offset with turf.buffer method, then create difference between polygon and buffer turf.difference method. Resulting polygon is inner border. If negative buffer polygon is empty, whole polygon gets role of inner border.
  • Use buffer with negative offset as polygon interior. If negative buffer is empty, polygon has no interior.

Polygon with custom border is then feature group consisting of those three polygons.

Up till here things are relatively simple, but if requirement is that such polygon can be created, edited and deleted via leaflet.draw plugin, things become quite complicated.

Code below is proof of concept that it can be done:

var polygonOutlines = L.featureGroup().addTo(map);

var deleteActive = false;
var deleteCandidates = [];

function checkDeleteRequest(polygonGroup) {
  if (!deleteActive) return;
  
  deleteCandidates.push(polygonGroup);
  polygonGroup.borderData.polygon.fire('click');
  map.removeLayer(polygonGroup);
}

function createCustomPolygonBorder(polygonGroup) {
  var outerBorderStyle = polygonGroup.borderData.outerBorderStyle;
  var innerBorderStyle = polygonGroup.borderData.innerBorderStyle;
  var innerPolygonStyle = polygonGroup.borderData.innerPolygonStyle;
  var polyGeoJSON = polygonGroup.borderData.polygon.toGeoJSON();
  
  var outerBuffer = turf.buffer(polyGeoJSON, outerBorderStyle.width, {units: 'meters'});
  var outerDiff = turf.difference(outerBuffer, polyGeoJSON);     
  var outerBorder = L.geoJSON(outerDiff, {
    weight: 0,
    lineJoin: "miter",
    fillOpacity: outerBorderStyle.opacity,
    fillColor: outerBorderStyle.color
  }).addTo(polygonGroup);
  if (polygonGroup.borderData.outerBorder) {
    polygonGroup.removeLayer(polygonGroup.borderData.outerBorder);
  }
  polygonGroup.borderData.outerBorder = outerBorder;
  
  var innerBuffer = turf.buffer(polyGeoJSON, -innerBorderStyle.width, {units: 'meters'});
  if (innerBuffer)
    var innerDiff = turf.difference(polyGeoJSON, innerBuffer);
  else {
    var innerDiff = polyGeoJSON;
  }
  var innerBorder = L.geoJSON(innerDiff, {
    weight: 0,
    lineJoin: "miter",
    fillOpacity: innerBorderStyle.opacity,
    fillColor: innerBorderStyle.color
  }).addTo(polygonGroup);
  if (polygonGroup.borderData.innerBorder) {
    polygonGroup.removeLayer(polygonGroup.borderData.innerBorder);
  }
  polygonGroup.borderData.innerBorder = innerBorder;       

  if (polygonGroup.borderData.innerPolygon) {
    polygonGroup.removeLayer(polygonGroup.borderData.innerPolygon);
    polygonGroup.borderData.innerPolygon = null;
  }
  if (innerBuffer) {
    var innerPolygon = L.geoJSON(innerBuffer, {
      weight: 0,
      lineJoin: "miter",
      fillOpacity: innerPolygonStyle.fillOpacity,
      fillColor: innerPolygonStyle.fillColor
    }).addTo(polygonGroup);
    innerPolygon.on('click', function(evt) {
      checkDeleteRequest(polygonGroup);
    });
    polygonGroup.borderData.innerPolygon = innerPolygon;
  }
}

function polygonWithCustomBorder(coords, polyStyle, outerBorderStyle, innerBorderStyle) {
  var polygonGroup = L.layerGroup({
    interactive: true
  });

  var polygon = L.polygon(coords, {
    stroke: false,
    fill: true,
    fillOpacity: 0,
  });
  polygon.options.polygonGroup = polygonGroup;
  polygon.addTo(polygonOutlines);
  
  polygonGroup.borderData = {};
  polygonGroup.borderData.polygon = polygon;
  polygonGroup.borderData.latLngs = polygon.getLatLngs();
  polygonGroup.borderData.isModified = false;
  polygonGroup.borderData.outerBorderStyle = outerBorderStyle;
  polygonGroup.borderData.innerBorderStyle = innerBorderStyle;
  polygonGroup.borderData.innerPolygonStyle = polyStyle;
  
  createCustomPolygonBorder(polygonGroup);

  return polygonGroup;
}

var drawnItems = new L.featureGroup().addTo(map);
var drawControl = new L.Control.Draw({
  draw: {
    marker: false,
    polyline: false,
    rectangle: false,
    circle: false,
    circlemarker: false,
    polygon: {
      allowIntersection: false,
      showArea: true
    }
  },
  edit: {
    featureGroup: polygonOutlines,
    poly: {
      allowIntersection: false
    }
  }
});
map.addControl(drawControl);

map.on('draw:created', function (event) {
  var layer = event.layer;

  var geoJSON = turf.flip(layer.toGeoJSON());
  var coords = geoJSON.geometry.coordinates[0];
  var polygon = polygonWithCustomBorder(
    coords,
    {fillOpacity: 0.2, fillColor: 'red'},
    {width: 5, color: 'blue', opacity: 0.5},
    {width: 3, color: 'red', opacity: 0.5}
  );
  polygon.addTo(map);
});

map.on('draw:editstart', function (event) {
  console.log('draw:editstart', event);
  polygonOutlines.setStyle({
    stroke: true,
  });
});
map.on('draw:editvertex', function (event) {
  var polygonGroup = event.poly.options.polygonGroup;
  polygonGroup.borderData.isModified = true;
  createCustomPolygonBorder(polygonGroup);
});
map.on('draw:editstop', function (event) {
  console.log('draw:editstop', event);
  polygonOutlines.eachLayer(function(layer) {
    var borderData = layer.options.polygonGroup.borderData;
    if (borderData.isModified) {
      borderData.isModified = false;
      createCustomPolygonBorder(layer.options.polygonGroup);
    }
  });
  polygonOutlines.setStyle({
    stroke: false,
  });
});
map.on('draw:edited', function (event) {
  var layers = event.layers;
  layers.eachLayer(function(layer) {
    var borderData = layer.options.polygonGroup.borderData;
    borderData.polygon = layer;
    borderData.isModified = false;
    borderData.latLngs = layer.getLatLngs();
  });
});

map.on('draw:deletestart', function (event) {
  deleteActive = true;
  polygonOutlines.setStyle({
    stroke: true,
    fill: true,
    dashArray: '8 3',
    lineCap: 'butt'
  });
});
map.on('draw:deletestop', function (event) {
  deleteActive = false;
  polygonOutlines.setStyle({
    stroke: false,
    fill: false,
    dashArray: null
  });
  deleteCandidates.forEach(function(layer) {
    layer.addTo(map);
  });
  deleteCandidates = [];
});
map.on('draw:deleted', function (event) {
  var layers = event.layers;
  layers.eachLayer(function(layer) {
    if (map.hasLayer(layer.options.polygonGroup)) {
      map.removeLayer(layer.options.polygonGroup);
    }
  });
  deleteCandidates = [];
});

Below is an example of direct polygon creation:

const coords= [
  [25.085416706126708,55.1561039686203],
  [25.085351117322578,55.15612542629241],
  [25.08549687017286,55.15618711709976],
  [25.08557703416652,55.15636146068573],
  [25.085567317321594,55.15644192695618],
  [25.085411847697962,55.15658676624298],
  [25.085487153321573,55.15664041042328],
  [25.085615901538485,55.156659185886376],
  [25.085598897064752,55.15631586313248],
  [25.085535737570268,55.15617907047272]
];   

var polygon = polygonWithCustomBorder(
  coords,
  {fillOpacity: 0.2, fillColor: 'red'},
  {width: 5, color: 'blue', opacity: 0.5},
  {width: 3, color: 'red', opacity: 0.5}
);
polygon.addTo(map);

Result looks like this:

enter image description here

Here is working JSFiddle: https://jsfiddle.net/TomazicM/p3hnk0tu/