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
Updated mapbox-gl-test-suite sha
  • Loading branch information
blanchg committed Jun 16, 2016
1 parent aa8e614 commit 8da9d99
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 21 deletions.
47 changes: 43 additions & 4 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 @@ -272,18 +274,22 @@ 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',
textRepeatDistance = symbolMinDistance / 2;
symbolPlacement = layout['symbol-placement'],
isLine = symbolPlacement === 'line',
textRepeatDistance = symbolMinDistance / 2,
polygons = null;

if (isLine) {
lines = clipLine(lines, 0, 0, EXTENT, EXTENT);
} else {
polygons = classifyRings(lines, 0);
}

for (var i = 0; i < lines.length; i++) {
var anchors = null;
var line = lines[i];

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

// For each potential label, create the placement features used to check for collisions, and the quads use for rendering.
Expand Down Expand Up @@ -332,6 +338,39 @@ SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feat
}
};

SymbolBucket.prototype.findPolygonAnchors = function(line, lines, polygons) {
var anchors = null;

// Work out what line is...
var polygon = null;
var isPolygon = false;
var isHole = false;
for (var i = 0; !isPolygon && !isHole && i < polygons.length; i++) {
polygon = polygons[i];
isPolygon = line === polygon[0];
if (!isPolygon) {
for (var j = 1; !isHole && j < polygon.length; j++) {
isHole = line === polygon[i];
}
}
}

if (isPolygon) {
var holes = null;
if (polygon.length > 1) {
holes = polygon.slice(1);
}
var poi = poleOfInaccessibility(line, holes, 1);
anchors = [ new Anchor(poi.x, poi.y, 0) ];
} else if (isHole) {
anchors = [];
} else {
anchors = [ new Anchor(line[0].x, line[0].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;
}
8 changes: 2 additions & 6 deletions js/util/intersection_tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

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

module.exports = {
multiPolygonIntersectsBufferedMultiPoint: multiPolygonIntersectsBufferedMultiPoint,
multiPolygonIntersectsMultiPolygon: multiPolygonIntersectsMultiPolygon,
Expand Down Expand Up @@ -98,12 +100,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
150 changes: 150 additions & 0 deletions js/util/polygon_poi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

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

function pointToLineDistance(x, y, x1, y1, x2, y2) {
var C = x2 - x1;
var D = y2 - y1;
var lenSq = C * C + D * D;
var param = -1;
if (lenSq !== 0) {
var A = x - x1;
var B = y - y1;
var dot = A * C + B * D;
param = dot / lenSq;
}
var xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
var dx = x - xx;
var dy = y - yy;
// return Math.sqrt(dx * dx + dy * dy);
return (dx * dx + dy * dy);
}

function pointToPerimeterDistance(x, y, points) {
var d, p1, p2, minDistance = Number.MAX_VALUE;
var len = points.length;

for (var i = -1, l = len, j = l - 1; ++i < l; j = i) {
p1 = points[i];
p2 = points[j];
d = pointToLineDistance(x, y, p1.x, p1.y, p2.x, p2.y);
if (d < minDistance) {
minDistance = d;
}
}
return minDistance;
}

function isInside(x, y, points) {
var c = false;
for (var i = -1, l = points.length, j = l - 1; ++i < l; j = i) {
var pi = points[i];
var pj = points[j];
if (((pi.y <= y && y < pj.y) || (pj.y <= y && y < pi.y)) &&
(x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x))
c = !c;
}
return c;
}

function poleScan(xMin, yMin, xMax, yMax, points, holes, allpoints) {
var px, py, pd, maxDistance = 0;
// If this is too small then we might miss large areas of the model
// If it is too big then we waste cpu searching
var splits = 24;
// This loop is to prevent some theoretical geometries from failing completely
while (maxDistance === 0) {
// Sample a grid of points over the shape, starting at the north western edge
var yStep = ((yMax - yMin) / splits);
var xStep = ((xMax - xMin) / splits);
for (var y = yMax; y >= yMin; y -= yStep) {
for (var x = xMin; x <= xMax; x += xStep) {
// Validate this point is inside the polygon
if (isInside(x, y, points)) {
if (holes) {
var inHole = false;
// And it isn't inside a hole
for (var i = 0; i < holes.length && !inHole; i++) {
var hole = holes[i];
inHole = isInside(x, y, hole);
}
if (inHole)
continue;
}
// Work out the distance to the closest perimiter
pd = pointToPerimeterDistance(x, y, allpoints);
if (pd > maxDistance) {
maxDistance = pd;
px = x;
py = y;
}
}
}
}
// If we didn't find any points then split the polygon up more.
splits = splits * 2;
}
return {
x: px,
y: py
};
}

/**
* Finds an approximation of a polygon's Pole Of Inaccessibiliy https://en.wikipedia.org/wiki/Pole_of_inaccessibility
* Note that for perfect rectangles the pole found ISN'T in the center (though the center is a valid POI)
*
* @param {Array<Point>} points the outer ring of the polygon
* @param {Array<Array<Point>>} [holes] List of interior holes
* @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
*
* @returns {Point} Pole of Inaccessibiliy.
*/
module.exports = function (points, holes, precision) {
if (!isClosedPolygon(points)) {
return points[0];
}

if (precision === undefined) {
precision = 1;
}
var xMin, yMin, xMax, yMax;
var p = points[0];
xMin = xMax = p.x;
yMin = yMax = p.y;
for (var i = 1; i < points.length; i++) {
p = points[i];
if (xMin > p.x) xMin = p.x;
if (xMax < p.x) xMax = p.x;
if (yMin > p.y) yMin = p.y;
if (yMax < p.y) yMax = p.y;
}

var allpoints = holes ? points.concat.apply(points, holes) : points;
var lp = poleScan(xMin, yMin, xMax, yMax, points, holes, allpoints);
if (precision > 0) {
var r = ((xMax - xMin) * (yMax - yMin));
var dx, dy;
while (r > precision) {
lp = poleScan(xMin, yMin, xMax, yMax, points, holes, allpoints);
dx = (xMax - xMin) / 24;
dy = (yMax - yMin) / 24;
xMin = lp.x - dx;
xMax = lp.x + dx;
yMin = lp.y - dy;
yMax = lp.y + dy;
r = dx * dy;
}
}
return lp;
};
57 changes: 57 additions & 0 deletions js/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,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.001 ||
Math.abs(p1.y - p2.y) > 0.001) {
return false;
}

// polygon simplification can produce polygons with zero area and more than 3 points
return (Math.abs(exports.calculateSignedArea(points)) > 0.01);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"highlight.js": "9.3.0",
"istanbul": "^0.4.2",
"lodash": "^4.13.1",
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#b9fdcbf3f73e4e120315d3792f419a6ca2d836fc",
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#4689af9c78576dd33307eafa3e17b65d4d403264",
"nyc": "6.4.0",
"remark": "4.2.2",
"remark-html": "3.0.0",
Expand Down
41 changes: 41 additions & 0 deletions test/js/util/polygon_poi.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'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 point = [
new Point(0, 0)
];
var line = [
new Point(10, 10),
new Point(0, 0)
];
var emptyPolygon = [
new Point(20, 20),
new Point(0, 0),
new Point(10, 10),
new Point(20, 20)
];
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, 7),
new Point(6, 1),
new Point(2, 1)
];
t.deepEqual(poleOfInaccessibility(point), point[0]);
t.deepEqual(poleOfInaccessibility(line), line[0]);
t.deepEqual(poleOfInaccessibility(emptyPolygon), emptyPolygon[0]);
t.deepEqual(poleOfInaccessibility(closedRing), new Point(7.083333333333335, 2.9166666666666665));
t.deepEqual(poleOfInaccessibility(closedRing, [closedRingHole]), new Point(7.916666666666669, 5));

t.end();
});

0 comments on commit 8da9d99

Please sign in to comment.