diff --git a/package.json b/package.json index 7665ab1..19f56fc 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/polylabel.js b/polylabel.js index bca664d..76c11f3 100644 --- a/polylabel.js +++ b/polylabel.js @@ -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; @@ -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; @@ -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); } @@ -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; } @@ -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; @@ -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 diff --git a/test/test.js b/test/test.js index bc0419c..373e84b 100644 --- a/test/test.js +++ b/test/test.js @@ -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(); });