PageRenderTime 51ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/js/symbol/collision_tile.js

https://gitlab.com/hwhelchel/mapbox-gl-js
JavaScript | 299 lines | 202 code | 52 blank | 45 comment | 26 complexity | 8146be602332726a5dcf9dd6b94391bb MD5 | raw file
  1. 'use strict';
  2. var Point = require('point-geometry');
  3. var EXTENT = require('../data/bucket').EXTENT;
  4. var Grid = require('grid-index');
  5. module.exports = CollisionTile;
  6. /**
  7. * A collision tile used to prevent symbols from overlapping. It keep tracks of
  8. * where previous symbols have been placed and is used to check if a new
  9. * symbol overlaps with any previously added symbols.
  10. *
  11. * @class CollisionTile
  12. * @param {number} angle
  13. * @param {number} pitch
  14. * @private
  15. */
  16. function CollisionTile(angle, pitch, collisionBoxArray) {
  17. if (typeof angle === 'object') {
  18. var serialized = angle;
  19. collisionBoxArray = pitch;
  20. angle = serialized.angle;
  21. pitch = serialized.pitch;
  22. this.grid = new Grid(serialized.grid);
  23. this.ignoredGrid = new Grid(serialized.ignoredGrid);
  24. } else {
  25. this.grid = new Grid(EXTENT, 12, 6);
  26. this.ignoredGrid = new Grid(EXTENT, 12, 0);
  27. }
  28. this.angle = angle;
  29. this.pitch = pitch;
  30. var sin = Math.sin(angle),
  31. cos = Math.cos(angle);
  32. this.rotationMatrix = [cos, -sin, sin, cos];
  33. this.reverseRotationMatrix = [cos, sin, -sin, cos];
  34. // Stretch boxes in y direction to account for the map tilt.
  35. this.yStretch = 1 / Math.cos(pitch / 180 * Math.PI);
  36. // The amount the map is squished depends on the y position.
  37. // Sort of account for this by making all boxes a bit bigger.
  38. this.yStretch = Math.pow(this.yStretch, 1.3);
  39. this.collisionBoxArray = collisionBoxArray;
  40. if (collisionBoxArray.length === 0) {
  41. // the first collisionBoxArray is passed to a CollisionTile
  42. // tempCollisionBox
  43. collisionBoxArray.emplaceBack();
  44. var maxInt16 = 32767;
  45. //left
  46. collisionBoxArray.emplaceBack(0, 0, 0, -maxInt16, 0, maxInt16, maxInt16,
  47. 0, 0, 0, 0, 0, 0, 0, 0,
  48. 0);
  49. // right
  50. collisionBoxArray.emplaceBack(EXTENT, 0, 0, -maxInt16, 0, maxInt16, maxInt16,
  51. 0, 0, 0, 0, 0, 0, 0, 0,
  52. 0);
  53. // top
  54. collisionBoxArray.emplaceBack(0, 0, -maxInt16, 0, maxInt16, 0, maxInt16,
  55. 0, 0, 0, 0, 0, 0, 0, 0,
  56. 0);
  57. // bottom
  58. collisionBoxArray.emplaceBack(0, EXTENT, -maxInt16, 0, maxInt16, 0, maxInt16,
  59. 0, 0, 0, 0, 0, 0, 0, 0,
  60. 0);
  61. }
  62. this.tempCollisionBox = collisionBoxArray.get(0);
  63. this.edges = [
  64. collisionBoxArray.get(1),
  65. collisionBoxArray.get(2),
  66. collisionBoxArray.get(3),
  67. collisionBoxArray.get(4)
  68. ];
  69. }
  70. CollisionTile.prototype.serialize = function() {
  71. var data = {
  72. angle: this.angle,
  73. pitch: this.pitch,
  74. grid: this.grid.toArrayBuffer(),
  75. ignoredGrid: this.ignoredGrid.toArrayBuffer()
  76. };
  77. return {
  78. data: data,
  79. transferables: [data.grid, data.ignoredGrid]
  80. };
  81. };
  82. CollisionTile.prototype.minScale = 0.25;
  83. CollisionTile.prototype.maxScale = 2;
  84. /**
  85. * Find the scale at which the collisionFeature can be shown without
  86. * overlapping with other features.
  87. *
  88. * @param {CollisionFeature} collisionFeature
  89. * @returns {number} placementScale
  90. * @private
  91. */
  92. CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allowOverlap, avoidEdges) {
  93. var collisionBoxArray = this.collisionBoxArray;
  94. var minPlacementScale = this.minScale;
  95. var rotationMatrix = this.rotationMatrix;
  96. var yStretch = this.yStretch;
  97. for (var b = collisionFeature.boxStartIndex; b < collisionFeature.boxEndIndex; b++) {
  98. var box = collisionBoxArray.get(b);
  99. var anchorPoint = box.anchorPoint._matMult(rotationMatrix);
  100. var x = anchorPoint.x;
  101. var y = anchorPoint.y;
  102. var x1 = x + box.x1;
  103. var y1 = y + box.y1 * yStretch;
  104. var x2 = x + box.x2;
  105. var y2 = y + box.y2 * yStretch;
  106. box.bbox0 = x1;
  107. box.bbox1 = y1;
  108. box.bbox2 = x2;
  109. box.bbox3 = y2;
  110. if (!allowOverlap) {
  111. var blockingBoxes = this.grid.query(x1, y1, x2, y2);
  112. for (var i = 0; i < blockingBoxes.length; i++) {
  113. var blocking = collisionBoxArray.get(blockingBoxes[i]);
  114. var blockingAnchorPoint = blocking.anchorPoint._matMult(rotationMatrix);
  115. minPlacementScale = this.getPlacementScale(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking);
  116. if (minPlacementScale >= this.maxScale) {
  117. return minPlacementScale;
  118. }
  119. }
  120. }
  121. if (avoidEdges) {
  122. var rotatedCollisionBox;
  123. if (this.angle) {
  124. var reverseRotationMatrix = this.reverseRotationMatrix;
  125. var tl = new Point(box.x1, box.y1).matMult(reverseRotationMatrix);
  126. var tr = new Point(box.x2, box.y1).matMult(reverseRotationMatrix);
  127. var bl = new Point(box.x1, box.y2).matMult(reverseRotationMatrix);
  128. var br = new Point(box.x2, box.y2).matMult(reverseRotationMatrix);
  129. rotatedCollisionBox = this.tempCollisionBox;
  130. rotatedCollisionBox.anchorPointX = box.anchorPoint.x;
  131. rotatedCollisionBox.anchorPointY = box.anchorPoint.y;
  132. rotatedCollisionBox.x1 = Math.min(tl.x, tr.x, bl.x, br.x);
  133. rotatedCollisionBox.y1 = Math.min(tl.y, tr.x, bl.x, br.x);
  134. rotatedCollisionBox.x2 = Math.max(tl.x, tr.x, bl.x, br.x);
  135. rotatedCollisionBox.y2 = Math.max(tl.y, tr.x, bl.x, br.x);
  136. rotatedCollisionBox.maxScale = box.maxScale;
  137. } else {
  138. rotatedCollisionBox = box;
  139. }
  140. for (var k = 0; k < this.edges.length; k++) {
  141. var edgeBox = this.edges[k];
  142. minPlacementScale = this.getPlacementScale(minPlacementScale, box.anchorPoint, rotatedCollisionBox, edgeBox.anchorPoint, edgeBox);
  143. if (minPlacementScale >= this.maxScale) {
  144. return minPlacementScale;
  145. }
  146. }
  147. }
  148. }
  149. return minPlacementScale;
  150. };
  151. CollisionTile.prototype.queryRenderedSymbols = function(minX, minY, maxX, maxY, scale) {
  152. var sourceLayerFeatures = {};
  153. var result = [];
  154. var collisionBoxArray = this.collisionBoxArray;
  155. var rotationMatrix = this.rotationMatrix;
  156. var anchorPoint = new Point(minX, minY)._matMult(rotationMatrix);
  157. var queryBox = this.tempCollisionBox;
  158. queryBox.anchorX = anchorPoint.x;
  159. queryBox.anchorY = anchorPoint.y;
  160. queryBox.x1 = 0;
  161. queryBox.y1 = 0;
  162. queryBox.x2 = maxX - minX;
  163. queryBox.y2 = maxY - minY;
  164. queryBox.maxScale = scale;
  165. // maxScale is stored using a Float32. Convert `scale` to the stored Float32 value.
  166. scale = queryBox.maxScale;
  167. var searchBox = [
  168. anchorPoint.x + queryBox.x1 / scale,
  169. anchorPoint.y + queryBox.y1 / scale * this.yStretch,
  170. anchorPoint.x + queryBox.x2 / scale,
  171. anchorPoint.y + queryBox.y2 / scale * this.yStretch
  172. ];
  173. var blockingBoxKeys = this.grid.query(searchBox[0], searchBox[1], searchBox[2], searchBox[3]);
  174. var blockingBoxKeys2 = this.ignoredGrid.query(searchBox[0], searchBox[1], searchBox[2], searchBox[3]);
  175. for (var k = 0; k < blockingBoxKeys2.length; k++) {
  176. blockingBoxKeys.push(blockingBoxKeys2[k]);
  177. }
  178. for (var i = 0; i < blockingBoxKeys.length; i++) {
  179. var blocking = collisionBoxArray.get(blockingBoxKeys[i]);
  180. var sourceLayer = blocking.sourceLayerIndex;
  181. var featureIndex = blocking.featureIndex;
  182. if (sourceLayerFeatures[sourceLayer] === undefined) {
  183. sourceLayerFeatures[sourceLayer] = {};
  184. }
  185. if (!sourceLayerFeatures[sourceLayer][featureIndex]) {
  186. var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix);
  187. var minPlacementScale = this.getPlacementScale(this.minScale, anchorPoint, queryBox, blockingAnchorPoint, blocking);
  188. if (minPlacementScale >= scale) {
  189. sourceLayerFeatures[sourceLayer][featureIndex] = true;
  190. result.push(blockingBoxKeys[i]);
  191. }
  192. }
  193. }
  194. return result;
  195. };
  196. CollisionTile.prototype.getPlacementScale = function(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking) {
  197. // Find the lowest scale at which the two boxes can fit side by side without overlapping.
  198. // Original algorithm:
  199. var anchorDiffX = anchorPoint.x - blockingAnchorPoint.x;
  200. var anchorDiffY = anchorPoint.y - blockingAnchorPoint.y;
  201. var s1 = (blocking.x1 - box.x2) / anchorDiffX; // scale at which new box is to the left of old box
  202. var s2 = (blocking.x2 - box.x1) / anchorDiffX; // scale at which new box is to the right of old box
  203. var s3 = (blocking.y1 - box.y2) * this.yStretch / anchorDiffY; // scale at which new box is to the top of old box
  204. var s4 = (blocking.y2 - box.y1) * this.yStretch / anchorDiffY; // scale at which new box is to the bottom of old box
  205. if (isNaN(s1) || isNaN(s2)) s1 = s2 = 1;
  206. if (isNaN(s3) || isNaN(s4)) s3 = s4 = 1;
  207. var collisionFreeScale = Math.min(Math.max(s1, s2), Math.max(s3, s4));
  208. var blockingMaxScale = blocking.maxScale;
  209. var boxMaxScale = box.maxScale;
  210. if (collisionFreeScale > blockingMaxScale) {
  211. // After a box's maxScale the label has shrunk enough that the box is no longer needed to cover it,
  212. // so unblock the new box at the scale that the old box disappears.
  213. collisionFreeScale = blockingMaxScale;
  214. }
  215. if (collisionFreeScale > boxMaxScale) {
  216. // If the box can only be shown after it is visible, then the box can never be shown.
  217. // But the label can be shown after this box is not visible.
  218. collisionFreeScale = boxMaxScale;
  219. }
  220. if (collisionFreeScale > minPlacementScale &&
  221. collisionFreeScale >= blocking.placementScale) {
  222. // If this collision occurs at a lower scale than previously found collisions
  223. // and the collision occurs while the other label is visible
  224. // this this is the lowest scale at which the label won't collide with anything
  225. minPlacementScale = collisionFreeScale;
  226. }
  227. return minPlacementScale;
  228. };
  229. /**
  230. * Remember this collisionFeature and what scale it was placed at to block
  231. * later features from overlapping with it.
  232. *
  233. * @param {CollisionFeature} collisionFeature
  234. * @param {number} minPlacementScale
  235. * @private
  236. */
  237. CollisionTile.prototype.insertCollisionFeature = function(collisionFeature, minPlacementScale, ignorePlacement) {
  238. var grid = ignorePlacement ? this.ignoredGrid : this.grid;
  239. var collisionBoxArray = this.collisionBoxArray;
  240. for (var k = collisionFeature.boxStartIndex; k < collisionFeature.boxEndIndex; k++) {
  241. var box = collisionBoxArray.get(k);
  242. box.placementScale = minPlacementScale;
  243. if (minPlacementScale < this.maxScale) {
  244. grid.insert(k, box.bbox0, box.bbox1, box.bbox2, box.bbox3);
  245. }
  246. }
  247. };