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

WIP Gravitate Towards Centroids #62

Closed
Closed
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "polylabel",
"version": "1.0.5",
"name": "@datavis-tech/polylabel",
"version": "1.2.0",
"description": "A JS library for finding optimal label position inside a polygon",
"main": "polylabel.js",
"scripts": {
Expand Down
52 changes: 37 additions & 15 deletions polylabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ if (Queue.default) Queue = Queue.default; // temporary webpack fix
module.exports = polylabel;
module.exports.default = polylabel;

function polylabel(polygon, precision, debug) {
function polylabel(polygon, precision, debug, centroidWeight) {
precision = precision || 1.0;
centroidWeight = centroidWeight || 0;

// find the bounding box of the outer ring
var minX, minY, maxX, maxY;
Expand All @@ -25,24 +26,35 @@ function polylabel(polygon, precision, debug) {
var cellSize = Math.min(width, height);
var h = cellSize / 2;

if (cellSize === 0) return [minX, minY];
if (cellSize === 0) {
var degeneratePoleOfInaccessibility = [minX, minY];
degeneratePoleOfInaccessibility.distance = 0;
return degeneratePoleOfInaccessibility;
}

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

var centroidCell = getCentroidCell(polygon);

// take centroid as the first best guess
var bestCell = centroidCell;

// 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, polygon));
cellQueue.push(new Cell(x + h, y + h, h, polygon, centroidCell));
}
}

// take centroid as the first best guess
var bestCell = getCentroidCell(polygon);
// the fitness function to be maximized
function fitness(cell) {
return cell.d - cell.distanceToCentroid * centroidWeight;
}

// special case for rectangular polygons
var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon);
if (bboxCell.d > bestCell.d) bestCell = bboxCell;
var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon, centroidCell);
if (fitness(bboxCell) > fitness(bestCell)) bestCell = bboxCell;

var numProbes = cellQueue.length;

Expand All @@ -51,7 +63,7 @@ function polylabel(polygon, precision, debug) {
var cell = cellQueue.pop();

// update the best cell if we found a better one
if (cell.d > bestCell.d) {
if (fitness(cell) > fitness(bestCell)) {
bestCell = cell;
if (debug) console.log('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes);
}
Expand All @@ -61,10 +73,10 @@ function polylabel(polygon, precision, debug) {

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

Expand All @@ -73,21 +85,31 @@ function polylabel(polygon, precision, debug) {
console.log('best distance: ' + bestCell.d);
}

return [bestCell.x, bestCell.y];
var poleOfInaccessibility = [bestCell.x, bestCell.y];
poleOfInaccessibility.distance = bestCell.d;
return poleOfInaccessibility;
}

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

function Cell(x, y, h, polygon) {
function Cell(x, y, h, polygon, centroidCell) {
this.x = x; // cell center x
this.y = y; // cell center y
this.h = h; // half the cell size
this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon
this.distanceToCentroid = centroidCell ? pointToPointDist(this, centroidCell) : 0;
this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
}

// distance between two cells
function pointToPointDist(cellA, cellB) {
var dx = cellB.x - cellA.x;
var dy = cellB.y - cellA.y;
return Math.sqrt(dx * dx + dy * dy);
}

// signed distance from point to polygon outline (negative if point is outside)
function pointToPolygonDist(x, y, polygon) {
var inside = false;
Expand All @@ -107,7 +129,7 @@ function pointToPolygonDist(x, y, polygon) {
}
}

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

// get polygon centroid
Expand Down
20 changes: 15 additions & 5 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,38 @@ var water2 = require('./fixtures/water2.json');

test('finds pole of inaccessibility for water1 and precision 1', function (t) {
var p = polylabel(water1, 1);
t.same(p, [3865.85009765625, 2124.87841796875]);
t.same(p, Object.assign([3865.85009765625, 2124.87841796875], {
distance: 288.8493574779127
}));
t.end();
});

test('finds pole of inaccessibility for water1 and precision 50', function (t) {
var p = polylabel(water1, 50);
t.same(p, [3854.296875, 2123.828125]);
t.same(p, Object.assign([3854.296875, 2123.828125], {
distance: 278.5795872381558
}));
t.end();
});

test('finds pole of inaccessibility for water2 and default precision 1', function (t) {
var p = polylabel(water2);
t.same(p, [3263.5, 3263.5]);
t.same(p, Object.assign([3263.5, 3263.5], {
distance: 960.5
}));
t.end();
});

test('works on degenerate polygons', function (t) {
var p = polylabel([[[0, 0], [1, 0], [2, 0], [0, 0]]]);
t.same(p, [0, 0]);
t.same(p, Object.assign([0, 0], {
distance: 0
}));

p = polylabel([[[0, 0], [1, 0], [1, 1], [1, 0], [0, 0]]]);
t.same(p, [0, 0]);
t.same(p, Object.assign([0, 0], {
distance: 0
}));

t.end();
});