AutoPlaceUtil.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import { is } from '../../util/ModelUtil';
  2. import {
  3. getMid,
  4. asTRBL,
  5. getOrientation
  6. } from 'diagram-js/lib/layout/LayoutUtil';
  7. import {
  8. find,
  9. reduce
  10. } from 'min-dash';
  11. var DEFAULT_HORIZONTAL_DISTANCE = 50;
  12. var MAX_HORIZONTAL_DISTANCE = 250;
  13. // padding to detect element placement
  14. var PLACEMENT_DETECTION_PAD = 10;
  15. /**
  16. * Always try to place element right of source;
  17. * compute actual distance from previous nodes in flow.
  18. */
  19. export function getFlowNodePosition(source, element) {
  20. var sourceTrbl = asTRBL(source);
  21. var sourceMid = getMid(source);
  22. var horizontalDistance = getFlowNodeDistance(source, element);
  23. var orientation = 'left',
  24. rowSize = 80,
  25. margin = 30;
  26. if (is(source, 'bpmn:BoundaryEvent')) {
  27. orientation = getOrientation(source, source.host, -25);
  28. if (orientation.indexOf('top') !== -1) {
  29. margin *= -1;
  30. }
  31. }
  32. function getVerticalDistance(orient) {
  33. if (orient.indexOf('top') != -1) {
  34. return -1 * rowSize;
  35. } else if (orient.indexOf('bottom') != -1) {
  36. return rowSize;
  37. } else {
  38. return 0;
  39. }
  40. }
  41. var position = {
  42. x: sourceTrbl.right + horizontalDistance + element.width / 2,
  43. y: sourceMid.y + getVerticalDistance(orientation)
  44. };
  45. var escapeDirection = {
  46. y: {
  47. margin: margin,
  48. rowSize: rowSize
  49. }
  50. };
  51. return deconflictPosition(source, element, position, escapeDirection);
  52. }
  53. /**
  54. * Compute best distance between source and target,
  55. * based on existing connections to and from source.
  56. *
  57. * @param {djs.model.Shape} source
  58. * @param {djs.model.Shape} element
  59. *
  60. * @return {Number} distance
  61. */
  62. export function getFlowNodeDistance(source, element) {
  63. var sourceTrbl = asTRBL(source);
  64. // is connection a reference to consider?
  65. function isReference(c) {
  66. return is(c, 'bpmn:SequenceFlow');
  67. }
  68. function toTargetNode(weight) {
  69. return function(shape) {
  70. return {
  71. shape: shape,
  72. weight: weight,
  73. distanceTo: function(shape) {
  74. var shapeTrbl = asTRBL(shape);
  75. return shapeTrbl.left - sourceTrbl.right;
  76. }
  77. };
  78. };
  79. }
  80. function toSourceNode(weight) {
  81. return function(shape) {
  82. return {
  83. shape: shape,
  84. weight: weight,
  85. distanceTo: function(shape) {
  86. var shapeTrbl = asTRBL(shape);
  87. return sourceTrbl.left - shapeTrbl.right;
  88. }
  89. };
  90. };
  91. }
  92. // we create a list of nodes to take into consideration
  93. // for calculating the optimal flow node distance
  94. //
  95. // * weight existing target nodes higher than source nodes
  96. // * only take into account individual nodes once
  97. //
  98. var nodes = reduce([].concat(
  99. getTargets(source, isReference).map(toTargetNode(5)),
  100. getSources(source, isReference).map(toSourceNode(1))
  101. ), function(nodes, node) {
  102. // filter out shapes connected twice via source or target
  103. nodes[node.shape.id + '__weight_' + node.weight] = node;
  104. return nodes;
  105. }, {});
  106. // compute distances between source and incoming nodes;
  107. // group at the same time by distance and expose the
  108. // favourite distance as { fav: { count, value } }.
  109. var distancesGrouped = reduce(nodes, function(result, node) {
  110. var shape = node.shape,
  111. weight = node.weight,
  112. distanceTo = node.distanceTo;
  113. var fav = result.fav,
  114. currentDistance,
  115. currentDistanceCount,
  116. currentDistanceEntry;
  117. currentDistance = distanceTo(shape);
  118. // ignore too far away peers
  119. // or non-left to right modeled nodes
  120. if (currentDistance < 0 || currentDistance > MAX_HORIZONTAL_DISTANCE) {
  121. return result;
  122. }
  123. currentDistanceEntry = result[String(currentDistance)] =
  124. result[String(currentDistance)] || {
  125. value: currentDistance,
  126. count: 0
  127. };
  128. // inc diff count
  129. currentDistanceCount = currentDistanceEntry.count += 1 * weight;
  130. if (!fav || fav.count < currentDistanceCount) {
  131. result.fav = currentDistanceEntry;
  132. }
  133. return result;
  134. }, { });
  135. if (distancesGrouped.fav) {
  136. return distancesGrouped.fav.value;
  137. } else {
  138. return DEFAULT_HORIZONTAL_DISTANCE;
  139. }
  140. }
  141. /**
  142. * Always try to place text annotations top right of source.
  143. */
  144. export function getTextAnnotationPosition(source, element) {
  145. var sourceTrbl = asTRBL(source);
  146. var position = {
  147. x: sourceTrbl.right + element.width / 2,
  148. y: sourceTrbl.top - 50 - element.height / 2
  149. };
  150. var escapeDirection = {
  151. y: {
  152. margin: -30,
  153. rowSize: 20
  154. }
  155. };
  156. return deconflictPosition(source, element, position, escapeDirection);
  157. }
  158. /**
  159. * Always put element bottom right of source.
  160. */
  161. export function getDataElementPosition(source, element) {
  162. var sourceTrbl = asTRBL(source);
  163. var position = {
  164. x: sourceTrbl.right - 10 + element.width / 2,
  165. y: sourceTrbl.bottom + 40 + element.width / 2
  166. };
  167. var escapeDirection = {
  168. x: {
  169. margin: 30,
  170. rowSize: 30
  171. }
  172. };
  173. return deconflictPosition(source, element, position, escapeDirection);
  174. }
  175. /**
  176. * Always put element right of source per default.
  177. */
  178. export function getDefaultPosition(source, element) {
  179. var sourceTrbl = asTRBL(source);
  180. var sourceMid = getMid(source);
  181. // simply put element right next to source
  182. return {
  183. x: sourceTrbl.right + DEFAULT_HORIZONTAL_DISTANCE + element.width / 2,
  184. y: sourceMid.y
  185. };
  186. }
  187. /**
  188. * Returns all connected elements around the given source.
  189. *
  190. * This includes:
  191. *
  192. * - connected elements
  193. * - host connected elements
  194. * - attachers connected elements
  195. *
  196. * @param {djs.model.Shape} source
  197. * @param {djs.model.Shape} element
  198. *
  199. * @return {Array<djs.model.Shape>}
  200. */
  201. function getAutoPlaceClosure(source, element) {
  202. var allConnected = getConnected(source);
  203. if (source.host) {
  204. allConnected = allConnected.concat(getConnected(source.host));
  205. }
  206. if (source.attachers) {
  207. allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) {
  208. return shapes.concat(getConnected(attacher));
  209. }, []));
  210. }
  211. return allConnected;
  212. }
  213. /**
  214. * Return target at given position, if defined.
  215. *
  216. * This takes connected elements from host and attachers
  217. * into account, too.
  218. */
  219. export function getConnectedAtPosition(source, position, element) {
  220. var bounds = {
  221. x: position.x - (element.width / 2),
  222. y: position.y - (element.height / 2),
  223. width: element.width,
  224. height: element.height
  225. };
  226. var closure = getAutoPlaceClosure(source, element);
  227. return find(closure, function(target) {
  228. if (target === element) {
  229. return false;
  230. }
  231. var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
  232. return orientation === 'intersect';
  233. });
  234. }
  235. /**
  236. * Returns a new, position for the given element
  237. * based on the given element that is not occupied
  238. * by some element connected to source.
  239. *
  240. * Take into account the escapeDirection (where to move
  241. * on positining clashes) in the computation.
  242. *
  243. * @param {djs.model.Shape} source
  244. * @param {djs.model.Shape} element
  245. * @param {Point} position
  246. * @param {Object} escapeDelta
  247. *
  248. * @return {Point}
  249. */
  250. export function deconflictPosition(source, element, position, escapeDelta) {
  251. function nextPosition(existingElement) {
  252. var newPosition = {
  253. x: position.x,
  254. y: position.y
  255. };
  256. [ 'x', 'y' ].forEach(function(axis) {
  257. var axisDelta = escapeDelta[axis];
  258. if (!axisDelta) {
  259. return;
  260. }
  261. var dimension = axis === 'x' ? 'width' : 'height';
  262. var margin = axisDelta.margin,
  263. rowSize = axisDelta.rowSize;
  264. if (margin < 0) {
  265. newPosition[axis] = Math.min(
  266. existingElement[axis] + margin - element[dimension] / 2,
  267. position[axis] - rowSize + margin
  268. );
  269. } else {
  270. newPosition[axis] = Math.max(
  271. existingTarget[axis] + existingTarget[dimension] + margin + element[dimension] / 2,
  272. position[axis] + rowSize + margin
  273. );
  274. }
  275. });
  276. return newPosition;
  277. }
  278. var existingTarget;
  279. // deconflict position until free slot is found
  280. while ((existingTarget = getConnectedAtPosition(source, position, element))) {
  281. position = nextPosition(existingTarget);
  282. }
  283. return position;
  284. }
  285. // helpers //////////////////////
  286. function noneFilter() {
  287. return true;
  288. }
  289. function getConnected(element, connectionFilter) {
  290. return [].concat(
  291. getTargets(element, connectionFilter),
  292. getSources(element, connectionFilter)
  293. );
  294. }
  295. function getSources(shape, connectionFilter) {
  296. if (!connectionFilter) {
  297. connectionFilter = noneFilter;
  298. }
  299. return shape.incoming.filter(connectionFilter).map(function(c) {
  300. return c.source;
  301. });
  302. }
  303. function getTargets(shape, connectionFilter) {
  304. if (!connectionFilter) {
  305. connectionFilter = noneFilter;
  306. }
  307. return shape.outgoing.filter(connectionFilter).map(function(c) {
  308. return c.target;
  309. });
  310. }