You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

165 lines
4.8 KiB

  1. 'use strict';
  2. var Queue = require('tinyqueue');
  3. if (Queue.default) Queue = Queue.default; // temporary webpack fix
  4. module.exports = polylabel;
  5. module.exports.default = polylabel;
  6. function polylabel(polygon, precision, debug) {
  7. precision = precision || 1.0;
  8. // find the bounding box of the outer ring
  9. var minX, minY, maxX, maxY;
  10. for (var i = 0; i < polygon[0].length; i++) {
  11. var p = polygon[0][i];
  12. if (!i || p[0] < minX) minX = p[0];
  13. if (!i || p[1] < minY) minY = p[1];
  14. if (!i || p[0] > maxX) maxX = p[0];
  15. if (!i || p[1] > maxY) maxY = p[1];
  16. }
  17. var width = maxX - minX;
  18. var height = maxY - minY;
  19. var cellSize = Math.min(width, height);
  20. var h = cellSize / 2;
  21. if (cellSize === 0) {
  22. var degeneratePoleOfInaccessibility = [minX, minY];
  23. degeneratePoleOfInaccessibility.distance = 0;
  24. return degeneratePoleOfInaccessibility;
  25. }
  26. // a priority queue of cells in order of their "potential" (max distance to polygon)
  27. var cellQueue = new Queue(undefined, compareMax);
  28. // cover polygon with initial cells
  29. for (var x = minX; x < maxX; x += cellSize) {
  30. for (var y = minY; y < maxY; y += cellSize) {
  31. cellQueue.push(new Cell(x + h, y + h, h, polygon));
  32. }
  33. }
  34. // take centroid as the first best guess
  35. var bestCell = getCentroidCell(polygon);
  36. // special case for rectangular polygons
  37. var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon);
  38. if (bboxCell.d > bestCell.d) bestCell = bboxCell;
  39. var numProbes = cellQueue.length;
  40. while (cellQueue.length) {
  41. // pick the most promising cell from the queue
  42. var cell = cellQueue.pop();
  43. // update the best cell if we found a better one
  44. if (cell.d > bestCell.d) {
  45. bestCell = cell;
  46. if (debug) console.log('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes);
  47. }
  48. // do not drill down further if there's no chance of a better solution
  49. if (cell.max - bestCell.d <= precision) continue;
  50. // split the cell into four cells
  51. h = cell.h / 2;
  52. cellQueue.push(new Cell(cell.x - h, cell.y - h, h, polygon));
  53. cellQueue.push(new Cell(cell.x + h, cell.y - h, h, polygon));
  54. cellQueue.push(new Cell(cell.x - h, cell.y + h, h, polygon));
  55. cellQueue.push(new Cell(cell.x + h, cell.y + h, h, polygon));
  56. numProbes += 4;
  57. }
  58. if (debug) {
  59. console.log('num probes: ' + numProbes);
  60. console.log('best distance: ' + bestCell.d);
  61. }
  62. var poleOfInaccessibility = [bestCell.x, bestCell.y];
  63. poleOfInaccessibility.distance = bestCell.d;
  64. return poleOfInaccessibility;
  65. }
  66. function compareMax(a, b) {
  67. return b.max - a.max;
  68. }
  69. function Cell(x, y, h, polygon) {
  70. this.x = x; // cell center x
  71. this.y = y; // cell center y
  72. this.h = h; // half the cell size
  73. this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon
  74. this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
  75. }
  76. // signed distance from point to polygon outline (negative if point is outside)
  77. function pointToPolygonDist(x, y, polygon) {
  78. var inside = false;
  79. var minDistSq = Infinity;
  80. for (var k = 0; k < polygon.length; k++) {
  81. var ring = polygon[k];
  82. for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
  83. var a = ring[i];
  84. var b = ring[j];
  85. if ((a[1] > y !== b[1] > y) &&
  86. (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside;
  87. minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b));
  88. }
  89. }
  90. return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq);
  91. }
  92. // get polygon centroid
  93. function getCentroidCell(polygon) {
  94. var area = 0;
  95. var x = 0;
  96. var y = 0;
  97. var points = polygon[0];
  98. for (var i = 0, len = points.length, j = len - 1; i < len; j = i++) {
  99. var a = points[i];
  100. var b = points[j];
  101. var f = a[0] * b[1] - b[0] * a[1];
  102. x += (a[0] + b[0]) * f;
  103. y += (a[1] + b[1]) * f;
  104. area += f * 3;
  105. }
  106. if (area === 0) return new Cell(points[0][0], points[0][1], 0, polygon);
  107. return new Cell(x / area, y / area, 0, polygon);
  108. }
  109. // get squared distance from a point to a segment
  110. function getSegDistSq(px, py, a, b) {
  111. var x = a[0];
  112. var y = a[1];
  113. var dx = b[0] - x;
  114. var dy = b[1] - y;
  115. if (dx !== 0 || dy !== 0) {
  116. var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
  117. if (t > 1) {
  118. x = b[0];
  119. y = b[1];
  120. } else if (t > 0) {
  121. x += dx * t;
  122. y += dy * t;
  123. }
  124. }
  125. dx = px - x;
  126. dy = py - y;
  127. return dx * dx + dy * dy;
  128. }