123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- import { is } from '../../util/ModelUtil';
- import {
- getMid,
- asTRBL,
- getOrientation
- } from 'diagram-js/lib/layout/LayoutUtil';
- import {
- find,
- reduce
- } from 'min-dash';
- var DEFAULT_HORIZONTAL_DISTANCE = 50;
- var MAX_HORIZONTAL_DISTANCE = 250;
- // padding to detect element placement
- var PLACEMENT_DETECTION_PAD = 10;
- /**
- * Always try to place element right of source;
- * compute actual distance from previous nodes in flow.
- */
- export function getFlowNodePosition(source, element) {
- var sourceTrbl = asTRBL(source);
- var sourceMid = getMid(source);
- var horizontalDistance = getFlowNodeDistance(source, element);
- var orientation = 'left',
- rowSize = 80,
- margin = 30;
- if (is(source, 'bpmn:BoundaryEvent')) {
- orientation = getOrientation(source, source.host, -25);
- if (orientation.indexOf('top') !== -1) {
- margin *= -1;
- }
- }
- function getVerticalDistance(orient) {
- if (orient.indexOf('top') != -1) {
- return -1 * rowSize;
- } else if (orient.indexOf('bottom') != -1) {
- return rowSize;
- } else {
- return 0;
- }
- }
- var position = {
- x: sourceTrbl.right + horizontalDistance + element.width / 2,
- y: sourceMid.y + getVerticalDistance(orientation)
- };
- var escapeDirection = {
- y: {
- margin: margin,
- rowSize: rowSize
- }
- };
- return deconflictPosition(source, element, position, escapeDirection);
- }
- /**
- * Compute best distance between source and target,
- * based on existing connections to and from source.
- *
- * @param {djs.model.Shape} source
- * @param {djs.model.Shape} element
- *
- * @return {Number} distance
- */
- export function getFlowNodeDistance(source, element) {
- var sourceTrbl = asTRBL(source);
- // is connection a reference to consider?
- function isReference(c) {
- return is(c, 'bpmn:SequenceFlow');
- }
- function toTargetNode(weight) {
- return function(shape) {
- return {
- shape: shape,
- weight: weight,
- distanceTo: function(shape) {
- var shapeTrbl = asTRBL(shape);
- return shapeTrbl.left - sourceTrbl.right;
- }
- };
- };
- }
- function toSourceNode(weight) {
- return function(shape) {
- return {
- shape: shape,
- weight: weight,
- distanceTo: function(shape) {
- var shapeTrbl = asTRBL(shape);
- return sourceTrbl.left - shapeTrbl.right;
- }
- };
- };
- }
- // we create a list of nodes to take into consideration
- // for calculating the optimal flow node distance
- //
- // * weight existing target nodes higher than source nodes
- // * only take into account individual nodes once
- //
- var nodes = reduce([].concat(
- getTargets(source, isReference).map(toTargetNode(5)),
- getSources(source, isReference).map(toSourceNode(1))
- ), function(nodes, node) {
- // filter out shapes connected twice via source or target
- nodes[node.shape.id + '__weight_' + node.weight] = node;
- return nodes;
- }, {});
- // compute distances between source and incoming nodes;
- // group at the same time by distance and expose the
- // favourite distance as { fav: { count, value } }.
- var distancesGrouped = reduce(nodes, function(result, node) {
- var shape = node.shape,
- weight = node.weight,
- distanceTo = node.distanceTo;
- var fav = result.fav,
- currentDistance,
- currentDistanceCount,
- currentDistanceEntry;
- currentDistance = distanceTo(shape);
- // ignore too far away peers
- // or non-left to right modeled nodes
- if (currentDistance < 0 || currentDistance > MAX_HORIZONTAL_DISTANCE) {
- return result;
- }
- currentDistanceEntry = result[String(currentDistance)] =
- result[String(currentDistance)] || {
- value: currentDistance,
- count: 0
- };
- // inc diff count
- currentDistanceCount = currentDistanceEntry.count += 1 * weight;
- if (!fav || fav.count < currentDistanceCount) {
- result.fav = currentDistanceEntry;
- }
- return result;
- }, { });
- if (distancesGrouped.fav) {
- return distancesGrouped.fav.value;
- } else {
- return DEFAULT_HORIZONTAL_DISTANCE;
- }
- }
- /**
- * Always try to place text annotations top right of source.
- */
- export function getTextAnnotationPosition(source, element) {
- var sourceTrbl = asTRBL(source);
- var position = {
- x: sourceTrbl.right + element.width / 2,
- y: sourceTrbl.top - 50 - element.height / 2
- };
- var escapeDirection = {
- y: {
- margin: -30,
- rowSize: 20
- }
- };
- return deconflictPosition(source, element, position, escapeDirection);
- }
- /**
- * Always put element bottom right of source.
- */
- export function getDataElementPosition(source, element) {
- var sourceTrbl = asTRBL(source);
- var position = {
- x: sourceTrbl.right - 10 + element.width / 2,
- y: sourceTrbl.bottom + 40 + element.width / 2
- };
- var escapeDirection = {
- x: {
- margin: 30,
- rowSize: 30
- }
- };
- return deconflictPosition(source, element, position, escapeDirection);
- }
- /**
- * Always put element right of source per default.
- */
- export function getDefaultPosition(source, element) {
- var sourceTrbl = asTRBL(source);
- var sourceMid = getMid(source);
- // simply put element right next to source
- return {
- x: sourceTrbl.right + DEFAULT_HORIZONTAL_DISTANCE + element.width / 2,
- y: sourceMid.y
- };
- }
- /**
- * Returns all connected elements around the given source.
- *
- * This includes:
- *
- * - connected elements
- * - host connected elements
- * - attachers connected elements
- *
- * @param {djs.model.Shape} source
- * @param {djs.model.Shape} element
- *
- * @return {Array<djs.model.Shape>}
- */
- function getAutoPlaceClosure(source, element) {
- var allConnected = getConnected(source);
- if (source.host) {
- allConnected = allConnected.concat(getConnected(source.host));
- }
- if (source.attachers) {
- allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) {
- return shapes.concat(getConnected(attacher));
- }, []));
- }
- return allConnected;
- }
- /**
- * Return target at given position, if defined.
- *
- * This takes connected elements from host and attachers
- * into account, too.
- */
- export function getConnectedAtPosition(source, position, element) {
- var bounds = {
- x: position.x - (element.width / 2),
- y: position.y - (element.height / 2),
- width: element.width,
- height: element.height
- };
- var closure = getAutoPlaceClosure(source, element);
- return find(closure, function(target) {
- if (target === element) {
- return false;
- }
- var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
- return orientation === 'intersect';
- });
- }
- /**
- * Returns a new, position for the given element
- * based on the given element that is not occupied
- * by some element connected to source.
- *
- * Take into account the escapeDirection (where to move
- * on positining clashes) in the computation.
- *
- * @param {djs.model.Shape} source
- * @param {djs.model.Shape} element
- * @param {Point} position
- * @param {Object} escapeDelta
- *
- * @return {Point}
- */
- export function deconflictPosition(source, element, position, escapeDelta) {
- function nextPosition(existingElement) {
- var newPosition = {
- x: position.x,
- y: position.y
- };
- [ 'x', 'y' ].forEach(function(axis) {
- var axisDelta = escapeDelta[axis];
- if (!axisDelta) {
- return;
- }
- var dimension = axis === 'x' ? 'width' : 'height';
- var margin = axisDelta.margin,
- rowSize = axisDelta.rowSize;
- if (margin < 0) {
- newPosition[axis] = Math.min(
- existingElement[axis] + margin - element[dimension] / 2,
- position[axis] - rowSize + margin
- );
- } else {
- newPosition[axis] = Math.max(
- existingTarget[axis] + existingTarget[dimension] + margin + element[dimension] / 2,
- position[axis] + rowSize + margin
- );
- }
- });
- return newPosition;
- }
- var existingTarget;
- // deconflict position until free slot is found
- while ((existingTarget = getConnectedAtPosition(source, position, element))) {
- position = nextPosition(existingTarget);
- }
- return position;
- }
- // helpers //////////////////////
- function noneFilter() {
- return true;
- }
- function getConnected(element, connectionFilter) {
- return [].concat(
- getTargets(element, connectionFilter),
- getSources(element, connectionFilter)
- );
- }
- function getSources(shape, connectionFilter) {
- if (!connectionFilter) {
- connectionFilter = noneFilter;
- }
- return shape.incoming.filter(connectionFilter).map(function(c) {
- return c.source;
- });
- }
- function getTargets(shape, connectionFilter) {
- if (!connectionFilter) {
- connectionFilter = noneFilter;
- }
- return shape.outgoing.filter(connectionFilter).map(function(c) {
- return c.target;
- });
- }
|