3

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

3 Answers 3

3

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/[email protected]/dist/leaflet.js"></script>
<link href="https://unpkg.com/[email protected]/dist/leaflet.css" rel="stylesheet"/>

<div id="map" style="width: 100vw; height: 100vw">0012</div>

Ideal Solution:

  • Implement a plugin as a separate npm module.
  • Try to extend or hook into Renderer itself instead of separately extending L.Canvas and L.SVG.
  • Hook the cusomization into base class Path instead of individual shapes: Polygon, Polyline or Circle.
Sign up to request clarification or add additional context in comments.

Comments

0

Use the Recatngle/Polygon method.

// define rectangle geographical bounds
var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
// create an orange rectangle
L.rectangle(bounds, {}).addTo(map);

The Use options to get the desired effect on the lines. Options are inherited from polyline options

There you can tweak color, opacity, fill, fillColor, fillOpacity and fillRule to get the desired effect on the lines

4 Comments

thanks, But It is a polygon, not a rectangle, the image and code in question is just for example.
You can use the same polyline options for polygons too. They all inherit from the same base calss.
yes, we can use the options but what if the shape is not the exact Rectangle. it can be a hexagonal shape
You can get any shape using Polygon. And use options to shape the lines the way you want.
0

The accepted answer does provide the correct approach. However, the specific implementation does not really add an inset border. It scales toward the center of the polygon. This works for squares and circles, but not for any non uniformly shaped polygon.

This solution (which is for OpenLayers!) solves this mathematically correct: OpenLayers 3: Offset stroke style

This can be adapted into the previous solution, as follows.

I am using this successfully with very odd shapes.

Hint: For very acute angles, set the lineJoin to 'miter' in the extraStyle definition.

import * as L from "leaflet";
import * as math from "mathjs";

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 originalParts = layer._parts.slice()

    // Draw extra styles
    if (Array.isArray(layer.options.extraStyles)) {
      const originalStyleProperties = styleProperties.reduce(
        (acc, cur) => ({...acc, [cur]: layer.options[cur]}),
        {}
      );

      for (let extraStyle of layer.options.extraStyles) {
        const offsetDistance = extraStyle.inset || 1

        layer._parts = layer._parts.map(points => {
          const coordinates = []
          let counter = 0

          for (let i = 0; i < points.length - 1; ++i) {

            // get each pair of points (each line)
            const from = points[i]
            const to = points[i+1]

            // calculate angle of the line
            const angle = Math.atan2(to.y - from.y, to.x - from.x)

            // offset the line ( the line's points)
            const newFrom = [Math.sin(angle) * offsetDistance + from.x, -Math.cos(angle) * offsetDistance + from.y]
            const newTo = [Math.sin(angle) * offsetDistance + to.x, -Math.cos(angle) * offsetDistance + to.y]

            coordinates.push(newFrom)
            coordinates.push(newTo)

            // When two lines cross each other, the inset will be drawn overlaying itself.
            // So if lines intersect, find the coordinate of WHERE they intersect and use this as new common endpoint for the two lines.
            if (coordinates.length > 2) {
              const intersection = math.intersect(coordinates[counter], coordinates[counter + 1], coordinates[counter + 2], coordinates[counter + 3])
              coordinates[counter + 1] = (intersection) ? intersection : coordinates[counter + 1]
              coordinates[counter + 2] = (intersection) ? intersection : coordinates[counter + 2]
              counter += 2
            }
          }

          // convert Array of [[x,y], [x,y],...] to Points
          return coordinates.map(semiPoint => {
            return {
              x: semiPoint[0],
              y: semiPoint[1],
            }
          })
        })

        Object.keys(extraStyle).map(k => (layer.options[k] = extraStyle[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)
  }
})


L.Canvas.withExtraStyles = function (options) {
  return new L.Canvas.WithExtraStyles(options)
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.