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:

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