Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use polygon centroid for 'point' symbol placements #3038

Merged
merged 1 commit into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 findPoleOfInaccessibility = require('../../util/find_pole_of_inaccessibility');
var classifyRings = require('../../util/classify_rings');

var shapeText = Shaping.shapeText;
var shapeIcon = Shaping.shapeIcon;
Expand Down Expand Up @@ -278,19 +280,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 @@ -303,9 +313,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 @@ -338,6 +352,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 = findPoleOfInaccessibility(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;
}
127 changes: 127 additions & 0 deletions js/util/find_pole_of_inaccessibility.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);
}
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
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 @@ -59,7 +60,7 @@
"istanbul": "^0.4.2",
"json-loader": "^0.5.4",
"lodash": "^4.13.1",
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#15e18375321393364907208715ab82c5a9c70f60",
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#3e36b193a0c442a3fd863119f101afa6db97b32d",
"memory-fs": "^0.3.0",
"minifyify": "^7.0.1",
"npm-run-all": "^3.0.0",
Expand Down
25 changes: 25 additions & 0 deletions test/js/util/find_pole_of_inaccessibility.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 findPoleOfInaccessibility = require('../../../js/util/find_pole_of_inaccessibility');

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(findPoleOfInaccessibility([closedRing], 0.1), new Point(7.0703125, 2.9296875));
t.deepEqual(findPoleOfInaccessibility([closedRing, closedRingHole], 0.1), new Point(7.96875, 2.03125));

t.end();
});