DistributeElements.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {
  2. sortBy,
  3. forEach,
  4. filter
  5. } from 'min-dash';
  6. var AXIS_DIMENSIONS = {
  7. horizontal: [ 'x', 'width' ],
  8. vertical: [ 'y', 'height' ]
  9. };
  10. var THRESHOLD = 5;
  11. /**
  12. * Groups and filters elements and then trigger even distribution.
  13. */
  14. export default function DistributeElements(modeling) {
  15. this._modeling = modeling;
  16. this._filters = [];
  17. // register filter for filtering big elements
  18. this.registerFilter(function(elements, axis, dimension) {
  19. var elementsSize = 0,
  20. numOfShapes = 0,
  21. avgDimension;
  22. forEach(elements, function(element) {
  23. if (element.waypoints || element.labelTarget) {
  24. return;
  25. }
  26. elementsSize += element[dimension];
  27. numOfShapes += 1;
  28. });
  29. avgDimension = Math.round(elementsSize / numOfShapes);
  30. return filter(elements, function(element) {
  31. return element[dimension] < (avgDimension + 50);
  32. });
  33. });
  34. }
  35. DistributeElements.$inject = [ 'modeling' ];
  36. /**
  37. * Registers filter functions that allow external parties to filter
  38. * out certain elements.
  39. *
  40. * @param {Function} filterFn
  41. */
  42. DistributeElements.prototype.registerFilter = function(filterFn) {
  43. if (typeof filterFn !== 'function') {
  44. throw new Error('the filter has to be a function');
  45. }
  46. this._filters.push(filterFn);
  47. };
  48. /**
  49. * Distributes the elements with a given orientation
  50. *
  51. * @param {Array} elements [description]
  52. * @param {String} orientation [description]
  53. */
  54. DistributeElements.prototype.trigger = function(elements, orientation) {
  55. var modeling = this._modeling;
  56. var groups,
  57. distributableElements;
  58. if (elements.length < 3) {
  59. return;
  60. }
  61. this._setOrientation(orientation);
  62. distributableElements = this._filterElements(elements);
  63. groups = this._createGroups(distributableElements);
  64. // nothing to distribute
  65. if (groups.length <= 2) {
  66. return;
  67. }
  68. modeling.distributeElements(groups, this._axis, this._dimension);
  69. return groups;
  70. };
  71. /**
  72. * Filters the elements with provided filters by external parties
  73. *
  74. * @param {Array[Elements]} elements
  75. *
  76. * @return {Array[Elements]}
  77. */
  78. DistributeElements.prototype._filterElements = function(elements) {
  79. var filters = this._filters,
  80. axis = this._axis,
  81. dimension = this._dimension,
  82. distributableElements = [].concat(elements);
  83. if (!filters.length) {
  84. return elements;
  85. }
  86. forEach(filters, function(filterFn) {
  87. distributableElements = filterFn(distributableElements, axis, dimension);
  88. });
  89. return distributableElements;
  90. };
  91. /**
  92. * Create range (min, max) groups. Also tries to group elements
  93. * together that share the same range.
  94. *
  95. * @example
  96. * var distributableElements = [
  97. * {
  98. * range: {
  99. * min: 100,
  100. * max: 200
  101. * },
  102. * elements: [ { id: 'shape1', .. }]
  103. * }
  104. * ]
  105. *
  106. * @param {Array} elements
  107. *
  108. * @return {Array[Objects]}
  109. */
  110. DistributeElements.prototype._createGroups = function(elements) {
  111. var rangeGroups = [],
  112. self = this,
  113. axis = this._axis,
  114. dimension = this._dimension;
  115. if (!axis) {
  116. throw new Error('must have a defined "axis" and "dimension"');
  117. }
  118. // sort by 'left->right' or 'top->bottom'
  119. var sortedElements = sortBy(elements, axis);
  120. forEach(sortedElements, function(element, idx) {
  121. var elementRange = self._findRange(element, axis, dimension),
  122. range;
  123. var previous = rangeGroups[rangeGroups.length - 1];
  124. if (previous && self._hasIntersection(previous.range, elementRange)) {
  125. rangeGroups[rangeGroups.length - 1].elements.push(element);
  126. } else {
  127. range = { range: elementRange, elements: [ element ] };
  128. rangeGroups.push(range);
  129. }
  130. });
  131. return rangeGroups;
  132. };
  133. /**
  134. * Maps a direction to the according axis and dimension
  135. *
  136. * @param {String} direction 'horizontal' or 'vertical'
  137. */
  138. DistributeElements.prototype._setOrientation = function(direction) {
  139. var orientation = AXIS_DIMENSIONS[direction];
  140. this._axis = orientation[0];
  141. this._dimension = orientation[1];
  142. };
  143. /**
  144. * Checks if the two ranges intercept each other
  145. *
  146. * @param {Object} rangeA {min, max}
  147. * @param {Object} rangeB {min, max}
  148. *
  149. * @return {Boolean}
  150. */
  151. DistributeElements.prototype._hasIntersection = function(rangeA, rangeB) {
  152. return Math.max(rangeA.min, rangeA.max) >= Math.min(rangeB.min, rangeB.max) &&
  153. Math.min(rangeA.min, rangeA.max) <= Math.max(rangeB.min, rangeB.max);
  154. };
  155. /**
  156. * Returns the min and max values for an element
  157. *
  158. * @param {[type]} element [description]
  159. * @param {[type]} axis [description]
  160. * @param {[type]} dimension [description]
  161. *
  162. * @return {[type]} [description]
  163. */
  164. DistributeElements.prototype._findRange = function(element) {
  165. var axis = element[this._axis],
  166. dimension = element[this._dimension];
  167. return {
  168. min: axis + THRESHOLD,
  169. max: axis + dimension - THRESHOLD
  170. };
  171. };