javascriptleafletgis

Challenge for polygon display within leaflet


We have a specific design challenge for polygon display within leaflet (latest version).

We have polygons which are rendered with a solid border as well as a semi-transparent background. We are looking for a way to draw a solid borderline as well as a wider "inline" border and no background.

Note: the question is for polygons not rectangular. The below image and code is just for example.

Is there any way to achieve this?

var polygon = L.polygon([
  [ 51.72872938200587, -2.415618896484375 ],
  [ 51.72872938200587, -2.080535888671875 ],
  [ 51.901918172561714, -2.080535888671875 ],
  [ 51.901918172561714, -2.415618896484375 ],
  [ 51.72872938200587, -2.415618896484375 ]

],{
 color:'#2F538F',
 fillOpacity: 0.9,
 fillColor: '#BFBFBF',
}).addTo(map);

enter image description here


Solution

  • This is achievable by utilizing leaftlet's class extension system.

    To start with, leaflet's class diagram could be consulted to determine where the extension is needed. As a general rule, first try to extend classes towards the root, and prefer L.Class.extend over L.Class.include.

    Working Solution:

    Codesandbox

    One approach is hooking into the rendering process. In the following example, L.Canvas is extended to a custom L.Canvas.WithExtraStyles class (leaflet's plugin building guidelines). The custom Renderer is then provided to map.

    In this approach, note that multiple borders and fills (both inset and outset) could be provided using the extraStyles config.

    extraStyle custom property accepts Array of PathOptions. With an additional inset, whose value could be positive or a negative number of pixels representing the offset form the border of the main geometry. A negative value of inset will put the border outside of the original polygon.

    While implementing such customizations, special care must be taken to make sure leaflet is not considering the added customizations as separate geometric shapes. Otherwise interactive functionalities e.g. Polygon Edit or Leaflet Draw will have unexpected behaviour.

    // CanvasWithExtraStyles.js
    // First step is to provide a special renderer which accept configuration for extra borders.
    // Here L.Canvas is extended using Leaflet's class system
    const styleProperties = ['stroke', 'color', 'weight', 'opacity', 'fill', 'fillColor', 'fillOpacity'];
    
    /*
     * @class Polygon.MultiStyle
     * @aka L.Polygon.MultiStyle
     * @inherits L.Polygon
     */
    L.Canvas.WithExtraStyles = L.Canvas.extend({
      _updatePoly: function(layer, closed) {
        const centerCoord = layer.getCenter();
        const center = this._map.latLngToLayerPoint(centerCoord);
        const originalParts = layer._parts.slice();
    
        // Draw extra styles
        if (Array.isArray(layer.options.extraStyles)) {
          const originalStyleProperties = styleProperties.reduce(
            (acc, cur) => ({ ...acc, [cur]: layer.options[cur] }),
            {}
          );
          const cx = center.x;
          const cy = center.y;
    
          for (let eS of layer.options.extraStyles) {
            const i = eS.inset || 0;
    
            // For now, the algo doesn't support MultiPolygon
            // To have it support MultiPolygon, find centroid
            // of each MultiPolygon and perform the following
            layer._parts[0] = layer._parts[0].map(p => {
              return {
                x: p.x < cx ? p.x + i : p.x - i,
                y: p.y < cy ? p.y + i : p.y - i
              };
            });
    
            //Object.keys(eS).map(k => layer.options[k] = eS[k]);
            Object.keys(eS).map(k => (layer.options[k] = eS[k]));
            L.Canvas.prototype._updatePoly.call(this, layer, closed);
          }
    
          // Resetting original conf
          layer._parts = originalParts;
          Object.assign(layer.options, originalStyleProperties);
        }
    
        L.Canvas.prototype._updatePoly.call(this, layer, closed);
      }
    });
    // Leaflet's conventions to also provide factory methods for classes
    L.Canvas.withExtraStyles = function(options) {
      return new L.Canvas.WithExtraStyles(options);
    };
    
    
    // --------------------------------------------------------------
    
    // map.js
    const map = L.map("map", {
      center: [52.5145206, 13.3499977],
      zoom: 18,
      renderer: new L.Canvas.WithExtraStyles()
    });
    
    new L.tileLayer(
      "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}.png",
      {
        attribution: `attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attribution">CARTO</a>`,
        detectRetina: true
      }
    ).addTo(map);
    
    // Map center
    const { x, y } = map.getSize();
    
    // Left Polygon
    const polyStyle1 = {
      color: '#2f528f',
      extraStyles: [
        {
          color: 'transparent',
          weight: 10,
          fillColor: '#d9d9d9'
        }
      ]
    };
    
    // Sudo coordinates are generated form map container pixels
    const polygonCoords1 = [
      [0, 10],
      [300, 10],
      [300, 310],
      [0, 310]
    ].map(point => map.containerPointToLatLng(point));
    const polygon1 = new L.Polygon(polygonCoords1, polyStyle1);
    polygon1.addTo(map);
    
    // Right Polygon
    const polyStyle2 = {
      fillColor: "transparent",
      color: '#2f528f',
      extraStyles: [
        {
          inset: 6,
          color: '#d9d9d9',
          weight: 10
        }
      ]
    };
    
    const polygonCoords2 = [
      [340, 10],
      [640, 10],
      [640, 310],
      [340, 310]
    ].map(point => map.containerPointToLatLng(point));
    const polygon2 = new L.Polygon(polygonCoords2, polyStyle2);
    polygon2.addTo(map);
    <script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"></script>
    <link href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" rel="stylesheet"/>
    
    <div id="map" style="width: 100vw; height: 100vw">0012</div>

    Ideal Solution: