35

I have a bunch of leaflet polygons on a map I created. Each polygon represents something different. A specific set of information is displayed in a popup depending on the page the user is on. I need to find a way to make the "popup" bubble open in the center of the polygon it represents.

Each polygon is drawn using the following code:

var L20 = [
    [74.0995, -99.92615],
    [74.14008, -99.4043],
    [74.07691, -99.33838],
    [74.03617, -99.86023]
];



var L19 = [
    [74.02559, -99.84924],
    [74.06636, -99.32739],
    [74.0029, -99.26147],
    [73.96197, -99.77783]
];

var L18 = [
    [73.95142, -99.76684],
    [73.99235, -99.25048],
    [73.92889, -99.18456],
    [73.8878, -99.69543]
];

var set1 = L.polygon([L20, L19, L18], {
    color: "#fff",
    weight: 1,
    stroke: true,
    opacity: 0.05,
    fillColor: "#346B1F",

}).addTo(map);

The popup is drawn using the following code:

var popup = L.popup({})
    .setLatLng([73.64017, -100.32715])
    .setContent(content).openOn(map);
    var popup = L.popup();

So I need to find a way for .setLatLang to determin or be given the center of the polygon.

I came up with 3 solutions that may work, not sure how to go about it.

  1. find a way to use the coordinates of a polygon to determine the center of the polygon where the popup will open.

  2. call one point of the polygon, then offset the position of the popup.

  3. Use an id for each polygon, so each popup knows the box area (polygon) it can be opened in.

Can someone help me please?

ProgramFOX
  • 6,131
  • 11
  • 45
  • 51
Nxlevel
  • 788
  • 1
  • 7
  • 17
  • Related: https://stackoverflow.com/questions/1203135/what-is-the-fastest-way-to-find-the-visual-center-of-an-irregularly-shaped-pol – StayOnTarget Jul 11 '18 at 19:43

5 Answers5

73

Since some time Leaflet has built-in getCenter() method:

polygon.getBounds().getCenter();
yarl
  • 3,107
  • 1
  • 22
  • 28
  • 7
    This should be the accepted answer. Also note that you don't even need the polygon object (which I was stupidly trying to get for a while). You can run it on any Bounds object, therefore the same thing applies to a layer i.e layer.getBounds().getCenter(); – masterchief Sep 18 '14 at 04:20
  • 2
    For most shapes, the center of the bounding box will be close enough to the center of the polygon. Right triangles and L-shaped areas are examples where the difference is noticeable, but this is much better for most cases, I'd agree. – Steve Clanton Dec 05 '14 at 19:01
  • 3
    This won't work if your polygon is in the shape of a U. The center of the bounding box will put it outside of the polygon. – Kristopher Nov 17 '16 at 21:46
43

There are a few ways to approximate the centroid of a polygon.

The easiest (but least accurate method) is to get the center of the bounding box that contains the polygon, as yarl suggested, using polygon.getBounds().getCenter();

I originally answered the question with the formula for finding the centroid of the points, which can be found by averaging the coordinates of its vertices.

var getCentroid = function (arr) { 
    return arr.reduce(function (x,y) {
        return [x[0] + y[0]/arr.length, x[1] + y[1]/arr.length] 
    }, [0,0]) 
}

centerL20 = getCentroid(L20);

While the centroid of the points is a close enough approximation to trick me, a commenter pointed out that it is not the centroid of the polygon.

An implementation based on the formula for a centroid of a non-self-intersecting closed polygon gives the correct result:

var getCentroid2 = function (arr) {
    var twoTimesSignedArea = 0;
    var cxTimes6SignedArea = 0;
    var cyTimes6SignedArea = 0;

    var length = arr.length

    var x = function (i) { return arr[i % length][0] };
    var y = function (i) { return arr[i % length][1] };

    for ( var i = 0; i < arr.length; i++) {
        var twoSA = x(i)*y(i+1) - x(i+1)*y(i);
        twoTimesSignedArea += twoSA;
        cxTimes6SignedArea += (x(i) + x(i+1)) * twoSA;
        cyTimes6SignedArea += (y(i) + y(i+1)) * twoSA;
    }
    var sixSignedArea = 3 * twoTimesSignedArea;
    return [ cxTimes6SignedArea / sixSignedArea, cyTimes6SignedArea / sixSignedArea];        
}
Steve Clanton
  • 4,064
  • 3
  • 32
  • 38
  • 1
    Perfect, this is exactly what i was looking for. I just modified it to run this function on each page to determine the center for each polygon, works perfect....Thank you very much!!! – Nxlevel Apr 01 '14 at 22:05
  • 2
    This answer is incorrect. Imagine a rectangle that is slightly rounded on the right side. Your method puts the centroid along the right edge, since the two vertices on the left side are under-represented in the average compared to the arbitrarily large number on the right. This works if every point "weighs" the same, but not if every unit area "weighs" the same, which is really what everyone means when they talk about the centroid of a polygon. – JounceCracklePop Aug 21 '15 at 21:28
  • Thanks for pointing that out @CarlLeth. I have corrected the answer (I think). Can you double check it for me? – Steve Clanton Sep 24 '15 at 21:20
  • It doesn't work with U-polygons – WhatIsHTML Feb 16 '22 at 10:31
  • There are times the centroid is better and times the pole of accessibility is better. The polylabel library (referenced in Kristofer's answer) has used both to find the best answer. If you are trying to find the center of a polygon now, you should definitely prefer the https://github.com/mapbox/polylabel solution. – Steve Clanton Feb 21 '22 at 04:28
20

The problem you are trying to solve is called the pole of inaccessibility problem. Finding the best place to put a label in a polygon isn't completely solved by finding the center of the bounding box. Consider a polygon in the shape of the letter U. The center of the bounding box puts the label outside of the polygon. It took me forever to find this outstanding library: https://github.com/mapbox/polylabel

From the README.MD:

A fast algorithm for finding polygon pole of inaccessibility, the most distant internal point from the polygon outline (not to be confused with centroid), implemented as a JavaScript library. Useful for optimal placement of a text label on a polygon.

It's an iterative grid algorithm, inspired by paper by Garcia-Castellanos & Lombardo, 2007. Unlike the one in the paper, this algorithm:

  • guarantees finding global optimum within the given precision
  • is many times faster (10-40x)

Usage:

Given polygon coordinates in GeoJSON-like format and precision (1.0 by default), Polylabel returns the pole of inaccessibility coordinate in [x, y] format.

var p = polylabel(polygon, 1.0);

Pole of Inaccessibility

How the algorithm works:

This is an iterative grid-based algorithm, which starts by covering the polygon with big square cells and then iteratively splitting them in the order of the most promising ones, while aggressively pruning uninteresting cells.

  1. Generate initial square cells that fully cover the polygon (with cell size equal to either width or height, whichever is lower). Calculate distance from the center of each cell to the outer polygon, using negative value if the point is outside the polygon (detected by ray-casting).
  2. Put the cells into a priority queue sorted by the maximum potential distance from a point inside a cell, defined as a sum of the distance from the center and the cell radius (equal to cell_size * sqrt(2) / 2).
  3. Calculate the distance from the centroid of the polygon and pick it as the first "best so far".
  4. Pull out cells from the priority queue one by one. If a cell's distance is better than the current best, save it as such. Then, if the cell potentially contains a better solution that the current best (cell_max - best_dist > precision), split it into 4 children cells and put them in the queue.
  5. Stop the algorithm when we have exhausted the queue and return the best cell's center as the pole of inaccessibility. It will be guaranteed to be a global optimum within the given precision.

The most distant internal point from the polygon outline

Kristopher
  • 819
  • 14
  • 31
  • P.S. I like to reduce the precision to around .4 because it finds a more precise spot in my polygon. It causes a few more iterations of the code but it's not noticeable on my project where I am finding the pole of inaccessibility of ~150 polygons. – Kristopher Nov 30 '16 at 16:16
2

assuming each polygon has only 4 sides it is simple

var L20 = [
[74.0995, -99.92615],
[74.14008, -99.4043],
[74.07691, -99.33838],
[74.03617, -99.86023]
];

using this example get max and min lat: 74.03617 and 74.14008 respectively so same for long: -99.92615 and 99.33838 respectively

Then get the middle value for each: (max - min) / 2 = 0.051955 and -0.293885 then add them to the minimum amount

gives you a centre of 74.088125, -99.632265

GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • I can mathematically figure it out. I just want a code that can automatically determine the center. – Nxlevel Apr 01 '14 at 22:04
  • then steves answer is correct - however your question had three options at the bottom and its easier to learn writing your own code - i just addressed your question :-P – GrahamTheDev Apr 01 '14 at 22:10
1

To move a polygon into view and center it use:

    map.fitBounds(poly.getBounds())

This will also set the zoom level correctly.

braandl
  • 96
  • 1
  • 4