Skip to content

Commit

Permalink
Issue #1231 Use polygon centroid for 'point' symbol placements
Browse files Browse the repository at this point in the history
uses a copy of @mourner mapbox/polylabel to find it
  • Loading branch information
blanchg committed Aug 12, 2016
1 parent 454df46 commit 072f81d
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 24 deletions.
43 changes: 37 additions & 6 deletions js/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ var clipLine = require('../../symbol/clip_line');
var util = require('../../util/util');
var loadGeometry = require('../load_geometry');
var CollisionFeature = require('../../symbol/collision_feature');
var poleOfInaccessibility = require('../../util/polygon_poi');
var classifyRings = require('../../util/classify_rings');

var shapeText = Shaping.shapeText;
var shapeIcon = Shaping.shapeIcon;
Expand Down Expand Up @@ -273,19 +275,27 @@ SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feat
iconAlongLine = layout['icon-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line',
mayOverlap = layout['text-allow-overlap'] || layout['icon-allow-overlap'] ||
layout['text-ignore-placement'] || layout['icon-ignore-placement'],
isLine = layout['symbol-placement'] === 'line',
symbolPlacement = layout['symbol-placement'],
isLine = symbolPlacement === 'line',
textRepeatDistance = symbolMinDistance / 2;

var list = null;
if (isLine) {
lines = clipLine(lines, 0, 0, EXTENT, EXTENT);
list = clipLine(lines, 0, 0, EXTENT, EXTENT);
} else {
// Only care about looping through the outer rings
list = classifyRings(lines, 0);
}

for (var i = 0; i < lines.length; i++) {
var line = lines[i];
for (var i = 0; i < list.length; i++) {
var anchors = null;
// At this point it is a list of points for a line or a list of polygon rings
var pointsOrRings = list[i];
var line = null;

// Calculate the anchor points around which you want to place labels
var anchors;
if (isLine) {
line = pointsOrRings;
anchors = getAnchors(
line,
symbolMinDistance,
Expand All @@ -298,9 +308,13 @@ SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feat
EXTENT
);
} else {
anchors = [ new Anchor(line[0].x, line[0].y, 0) ];
line = pointsOrRings[0];
anchors = this.findPolygonAnchors(pointsOrRings);
}


// Here line is a list of points that is either the outer ring of a polygon or just a line

// For each potential label, create the placement features used to check for collisions, and the quads use for rendering.
for (var j = 0, len = anchors.length; j < len; j++) {
var anchor = anchors[j];
Expand Down Expand Up @@ -333,6 +347,23 @@ SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feat
}
};

SymbolBucket.prototype.findPolygonAnchors = function(polygonRings) {

var outerRing = polygonRings[0];
if (outerRing.length === 0) {
return [];
} else if (outerRing.length < 3 || !util.isClosedPolygon(outerRing)) {
return [ new Anchor(outerRing[0].x, outerRing[0].y, 0) ];
}

var anchors = null;
// 16 here represents 2 pixels
var poi = poleOfInaccessibility(polygonRings, 16);
anchors = [ new Anchor(poi.x, poi.y, 0) ];

return anchors;
};

SymbolBucket.prototype.anchorIsTooClose = function(text, repeatDistance, anchor) {
var compareText = this.compareText;
if (!(text in compareText)) {
Expand Down
11 changes: 1 addition & 10 deletions js/util/classify_rings.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var quickselect = require('quickselect');
var calculateSignedArea = require('./util').calculateSignedArea;

// classifies an array of rings into polygons with outer rings and holes
module.exports = function classifyRings(rings, maxRings) {
Expand Down Expand Up @@ -46,13 +47,3 @@ module.exports = function classifyRings(rings, maxRings) {
function compareAreas(a, b) {
return b.area - a.area;
}

function calculateSignedArea(ring) {
var sum = 0;
for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
p1 = ring[i];
p2 = ring[j];
sum += (p2.x - p1.x) * (p1.y + p2.y);
}
return sum;
}
11 changes: 4 additions & 7 deletions js/util/intersection_tests.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use strict';

var isCounterClockwise = require('./util').isCounterClockwise;

module.exports = {
multiPolygonIntersectsBufferedMultiPoint: multiPolygonIntersectsBufferedMultiPoint,
multiPolygonIntersectsMultiPolygon: multiPolygonIntersectsMultiPolygon,
multiPolygonIntersectsBufferedMultiLine: multiPolygonIntersectsBufferedMultiLine
multiPolygonIntersectsBufferedMultiLine: multiPolygonIntersectsBufferedMultiLine,
distToSegmentSquared: distToSegmentSquared
};

function multiPolygonIntersectsBufferedMultiPoint(multiPolygon, rings, radius) {
Expand Down Expand Up @@ -98,12 +101,6 @@ function lineIntersectsLine(lineA, lineB) {
return false;
}


// http://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
function isCounterClockwise(a, b, c) {
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
}

function lineSegmentIntersectsLineSegment(a0, a1, b0, b1) {
return isCounterClockwise(a0, b0, b1) !== isCounterClockwise(a1, b0, b1) &&
isCounterClockwise(a0, a1, b0) !== isCounterClockwise(a0, a1, b1);
Expand Down
127 changes: 127 additions & 0 deletions js/util/polygon_poi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use strict';
var Queue = require('tinyqueue');
var Point = require('point-geometry');
var distToSegmentSquared = require('./intersection_tests').distToSegmentSquared;

/**
* Finds an approximation of a polygon's Pole Of Inaccessibiliy https://en.wikipedia.org/wiki/Pole_of_inaccessibility
* This is a copy of http://github.com/mapbox/polylabel adapted to use Points
*
* @param {Array<Array<Point>>} List of polygon rings first item in array is the outer ring followed optionally by the list of holes, should be an element of the result of util/classify_rings
* @param {number} [precision=1] Specified in input coordinate units. If 0 returns after first run, if > 0 repeatedly narrows the search space until the radius of the area searched for the best pole is less than precision
* @param {bool} [debug=false] Print some statistics to the console during execution
*
* @returns {Point} Pole of Inaccessibiliy.
*/
module.exports = function (polygonRings, precision, debug) {
precision = precision || 1.0;

// find the bounding box of the outer ring
var minX, minY, maxX, maxY;
var outerRing = polygonRings[0];
for (var i = 0; i < outerRing.length; i++) {
var p = outerRing[i];
if (!i || p.x < minX) minX = p.x;
if (!i || p.y < minY) minY = p.y;
if (!i || p.x > maxX) maxX = p.x;
if (!i || p.y > maxY) maxY = p.y;
}

var width = maxX - minX;
var height = maxY - minY;
var cellSize = Math.min(width, height);
var h = cellSize / 2;

// a priority queue of cells in order of their "potential" (max distance to polygon)
var cellQueue = new Queue(null, compareMax);

// cover polygon with initial cells
for (var x = minX; x < maxX; x += cellSize) {
for (var y = minY; y < maxY; y += cellSize) {
cellQueue.push(new Cell(x + h, y + h, h, polygonRings));
}
}

// take centroid as the first best guess
var bestCell = getCentroidCell(polygonRings);
var numProbes = cellQueue.length;

while (cellQueue.length) {
// pick the most promising cell from the queue
var cell = cellQueue.pop();

// update the best cell if we found a better one
if (cell.d > bestCell.d) {
bestCell = cell;
if (debug) console.log('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes);
}

// do not drill down further if there's no chance of a better solution
if (cell.max - bestCell.d <= precision) continue;

// split the cell into four cells
h = cell.h / 2;
cellQueue.push(new Cell(cell.p.x - h, cell.p.y - h, h, polygonRings));
cellQueue.push(new Cell(cell.p.x + h, cell.p.y - h, h, polygonRings));
cellQueue.push(new Cell(cell.p.x - h, cell.p.y + h, h, polygonRings));
cellQueue.push(new Cell(cell.p.x + h, cell.p.y + h, h, polygonRings));
numProbes += 4;
}

if (debug) {
console.log('num probes: ' + numProbes);
console.log('best distance: ' + bestCell.d);
}

return bestCell.p;
};

function compareMax(a, b) {
return b.max - a.max;
}

function Cell(x, y, h, polygon) {
this.p = new Point(x, y);
this.h = h; // half the cell size
this.d = pointToPolygonDist(this.p, polygon); // distance from cell center to polygon
this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
}

// signed distance from point to polygon outline (negative if point is outside)
function pointToPolygonDist(p, polygon) {
var inside = false;
var minDistSq = Infinity;

for (var k = 0; k < polygon.length; k++) {
var ring = polygon[k];

for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
var a = ring[i];
var b = ring[j];

if ((a.y > p.y !== b.y > p.y) &&
(p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x)) inside = !inside;

minDistSq = Math.min(minDistSq, distToSegmentSquared(p, a, b));
}
}

return (inside ? 1 : -1) * Math.sqrt(minDistSq);
}

// get polygon centroid
function getCentroidCell(polygon) {
var area = 0;
var x = 0;
var y = 0;
var points = polygon[0];
for (var i = 0, len = points.length, j = len - 1; i < len; j = i++) {
var a = points[i];
var b = points[j];
var f = a.x * b.y - b.x * a.y;
x += (a.x + b.x) * f;
y += (a.y + b.y) * f;
area += f * 3;
}
return new Cell(x / area, y / area, 0, polygon);
}
57 changes: 57 additions & 0 deletions js/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,60 @@ exports.warnOnce = function(message) {
warnOnceHistory[message] = true;
}
};

/**
* Indicates if the provided Points are in a counter clockwise (true) or clockwise (false) order
*
* @param {Point} a
* @param {Point} b
* @param {Point} c
*
* @returns {boolean} true for a counter clockwise set of points
*/
// http://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
exports.isCounterClockwise = function(a, b, c) {
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
};

/**
* Returns the signed area for the polygon ring. Postive areas are exterior rings and
* have a clockwise winding. Negative areas are interior rings and have a counter clockwise
* ordering.
*
* @param {Array<Point>} ring - Exterior or interior ring
*
* @returns {number}
*/
exports.calculateSignedArea = function(ring) {
var sum = 0;
for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
p1 = ring[i];
p2 = ring[j];
sum += (p2.x - p1.x) * (p1.y + p2.y);
}
return sum;
};

/**
* Detects closed polygons, first + last point are equal
* @param {Array<Point>} points array of points
*
* @return {boolean} true if the points are a closed polygon
*/
exports.isClosedPolygon = function(points) {
// If it is 2 points that are the same then it is a point
// If it is 3 points with start and end the same then it is a line
if (points.length < 4)
return false;

var p1 = points[0];
var p2 = points[points.length - 1];

if (Math.abs(p1.x - p2.x) > 0 ||
Math.abs(p1.y - p2.y) > 0) {
return false;
}

// polygon simplification can produce polygons with zero area and more than 3 points
return (Math.abs(exports.calculateSignedArea(points)) > 0.01);
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"resolve-url": "^0.2.1",
"shelf-pack": "^1.0.0",
"supercluster": "^2.0.1",
"tinyqueue": "^1.1.0",
"unassertify": "^2.0.0",
"unitbezier": "^0.0.0",
"vector-tile": "^1.3.0",
Expand Down Expand Up @@ -62,7 +63,7 @@
"istanbul": "^0.4.2",
"json-loader": "^0.5.4",
"lodash": "^4.13.1",
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#1619d84e76ff3434becd51237720d370c7405ee5",
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#618f28de0d5d3b20f895c7363ce0b1df05cd465c",
"memory-fs": "^0.3.0",
"minifyify": "^7.0.1",
"nyc": "6.4.0",
Expand Down
25 changes: 25 additions & 0 deletions test/js/util/polygon_poi.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

var test = require('tap').test;
var Point = require('point-geometry');
var poleOfInaccessibility = require('../../../js/util/polygon_poi');

test('polygon_poi', function(t) {

var closedRing = [
new Point(0, 0),
new Point(10, 10),
new Point(10, 0),
new Point(0, 0)
];
var closedRingHole = [
new Point(2, 1),
new Point(6, 6),
new Point(6, 1),
new Point(2, 1)
];
t.deepEqual(poleOfInaccessibility([closedRing], 0.1), new Point(7.0703125, 2.9296875));
t.deepEqual(poleOfInaccessibility([closedRing, closedRingHole], 0.1), new Point(7.96875, 2.03125));

t.end();
});

0 comments on commit 072f81d

Please sign in to comment.