diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
317 lines (263 loc) • 8.22 kB
JavaScript
import {
asTRBL,
getOrientation,
getMid
} from '../../layout/LayoutUtil';
import {
find,
reduce
} from 'min-dash';
/**
* @typedef {import('../../model/Types').Connection} Connection
* @typedef {import('../../model/Types').Element} Element
* @typedef {import('../../model/Types').Shape} Shape
*
* @typedef {import('../../util/Types').Point} Point
*/
// padding to detect element placement
var PLACEMENT_DETECTION_PAD = 10;
export var DEFAULT_DISTANCE = 50;
var DEFAULT_MAX_DISTANCE = 250;
/**
* Get free position starting from given position.
*
* @param {Shape} source
* @param {Shape} element
* @param {Point} position
* @param {(element: Element, position: Point, connectedAtPosition: Element) => Point} getNextPosition
*
* @return {Point}
*/
export function findFreePosition(source, element, position, getNextPosition) {
var connectedAtPosition;
while ((connectedAtPosition = getConnectedAtPosition(source, position, element))) {
position = getNextPosition(element, position, connectedAtPosition);
}
return position;
}
/**
* Returns function that returns next position.
*
* @param {Object} nextPositionDirection
* @param {Object} [nextPositionDirection.x]
* @param {Object} [nextPositionDirection.y]
*
* @return {(element: Element, previousPosition: Point, connectedAtPosition: Element) => Point}
*/
export function generateGetNextPosition(nextPositionDirection) {
return function(element, previousPosition, connectedAtPosition) {
var nextPosition = {
x: previousPosition.x,
y: previousPosition.y
};
[ 'x', 'y' ].forEach(function(axis) {
var nextPositionDirectionForAxis = nextPositionDirection[ axis ];
if (!nextPositionDirectionForAxis) {
return;
}
var dimension = axis === 'x' ? 'width' : 'height';
var margin = nextPositionDirectionForAxis.margin,
minDistance = nextPositionDirectionForAxis.minDistance;
if (margin < 0) {
nextPosition[ axis ] = Math.min(
connectedAtPosition[ axis ] + margin - element[ dimension ] / 2,
previousPosition[ axis ] - minDistance + margin
);
} else {
nextPosition[ axis ] = Math.max(
connectedAtPosition[ axis ] + connectedAtPosition[ dimension ] + margin + element[ dimension ] / 2,
previousPosition[ axis ] + minDistance + margin
);
}
});
return nextPosition;
};
}
/**
* Return connected element at given position and within given bounds. Takes
* connected elements from host and attachers into account, too.
*
* @param {Shape} source
* @param {Point} position
* @param {Shape} element
*
* @return {Shape|undefined}
*/
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);
return find(closure, function(target) {
if (target === element) {
return false;
}
var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
return orientation === 'intersect';
});
}
/**
* Compute optimal distance between source and target based on existing connections to and from source.
* Assumes left-to-right and top-to-down modeling.
*
* @param {Shape} source
* @param {Object} [hints]
* @param {number} [hints.defaultDistance]
* @param {string} [hints.direction]
* @param {(connection: Connection) => boolean} [hints.filter]
* @param {(connection: Connection) => number} [hints.getWeight]
* @param {number} [hints.maxDistance]
* @param {'start'|'center'|'end'} [hints.reference]
*
* @return {number}
*/
export function getConnectedDistance(source, hints) {
if (!hints) {
hints = {};
}
// targets > sources by default
function getDefaultWeight(connection) {
return connection.source === source ? 1 : -1;
}
var defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE,
direction = hints.direction || 'e',
filter = hints.filter,
getWeight = hints.getWeight || getDefaultWeight,
maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE,
reference = hints.reference || 'start';
if (!filter) {
filter = noneFilter;
}
function getDistance(a, b) {
if (direction === 'n') {
if (reference === 'start') {
return asTRBL(a).top - asTRBL(b).bottom;
} else if (reference === 'center') {
return asTRBL(a).top - getMid(b).y;
} else {
return asTRBL(a).top - asTRBL(b).top;
}
} else if (direction === 'w') {
if (reference === 'start') {
return asTRBL(a).left - asTRBL(b).right;
} else if (reference === 'center') {
return asTRBL(a).left - getMid(b).x;
} else {
return asTRBL(a).left - asTRBL(b).left;
}
} else if (direction === 's') {
if (reference === 'start') {
return asTRBL(b).top - asTRBL(a).bottom;
} else if (reference === 'center') {
return getMid(b).y - asTRBL(a).bottom;
} else {
return asTRBL(b).bottom - asTRBL(a).bottom;
}
} else {
if (reference === 'start') {
return asTRBL(b).left - asTRBL(a).right;
} else if (reference === 'center') {
return getMid(b).x - asTRBL(a).right;
} else {
return asTRBL(b).right - asTRBL(a).right;
}
}
}
var sourcesDistances = source.incoming
.filter(filter)
.map(function(connection) {
var weight = getWeight(connection);
var distance = weight < 0
? getDistance(connection.source, source)
: getDistance(source, connection.source);
return {
id: connection.source.id,
distance: distance,
weight: weight
};
});
var targetsDistances = source.outgoing
.filter(filter)
.map(function(connection) {
var weight = getWeight(connection);
var distance = weight > 0
? getDistance(source, connection.target)
: getDistance(connection.target, source);
return {
id: connection.target.id,
distance: distance,
weight: weight
};
});
var distances = sourcesDistances.concat(targetsDistances).reduce(function(accumulator, currentValue) {
accumulator[ currentValue.id + '__weight_' + currentValue.weight ] = currentValue;
return accumulator;
}, {});
var distancesGrouped = reduce(distances, function(accumulator, currentValue) {
var distance = currentValue.distance,
weight = currentValue.weight;
if (distance < 0 || distance > maxDistance) {
return accumulator;
}
if (!accumulator[ String(distance) ]) {
accumulator[ String(distance) ] = 0;
}
accumulator[ String(distance) ] += 1 * weight;
if (!accumulator.distance || accumulator[ accumulator.distance ] < accumulator[ String(distance) ]) {
accumulator.distance = distance;
}
return accumulator;
}, {});
return distancesGrouped.distance || defaultDistance;
}
/**
* Returns all elements connected to given source.
*
* This includes:
*
* - elements connected to source
* - elements connected to host if source is an attacher
* - elements connected to attachers if source is a host
*
* @param {Shape} source
*
* @return {Shape[]}
*/
function getAutoPlaceClosure(source) {
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;
}
/**
* Get all connected elements.
*
* @param {Shape} element
*
* @returns {Shape[]}
*/
function getConnected(element) {
return getTargets(element).concat(getSources(element));
}
function getSources(shape) {
return shape.incoming.map(function(connection) {
return connection.source;
});
}
function getTargets(shape) {
return shape.outgoing.map(function(connection) {
return connection.target;
});
}
function noneFilter() {
return true;
}