element.
this.attr({
'.content': {
html: content
}
});
} else {
// Content element is a
element.
// SVG elements don't have innerHTML attribute.
this.attr({
'.content': {
text: content
}
});
}
},
// Here for backwards compatibility:
setForeignObjectSize: function() {
this.updateSize.apply(this, arguments);
},
// Here for backwards compatibility:
setDivContent: function() {
this.updateContent.apply(this, arguments);
}
});
// TextBlockView implements the fallback for IE when no foreignObject exists and
// the text needs to be manually broken.
joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({
initialize: function() {
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
// Keep this for backwards compatibility:
this.noSVGForeignObjectElement = !joint.env.test('svgforeignobject');
if (!joint.env.test('svgforeignobject')) {
this.listenTo(this.model, 'change:content', function(cell) {
// avoiding pass of extra paramters
this.updateContent(cell);
});
}
},
update: function(cell, renderingOnlyAttrs) {
if (joint.env.test('svgforeignobject')) {
var model = this.model;
// Update everything but the content first.
var noTextAttrs = _.omit(renderingOnlyAttrs || model.get('attrs'), '.content');
joint.dia.ElementView.prototype.update.call(this, model, noTextAttrs);
if (!renderingOnlyAttrs || _.has(renderingOnlyAttrs, '.content')) {
// Update the content itself.
this.updateContent(model, renderingOnlyAttrs);
}
} else {
joint.dia.ElementView.prototype.update.call(this, model, renderingOnlyAttrs);
}
},
updateContent: function(cell, renderingOnlyAttrs) {
// Create copy of the text attributes
var textAttrs = _.merge({}, (renderingOnlyAttrs || cell.get('attrs'))['.content']);
textAttrs = _.omit(textAttrs, 'text');
// Break the content to fit the element size taking into account the attributes
// set on the model.
var text = joint.util.breakText(cell.get('content'), cell.get('size'), textAttrs, {
// measuring sandbox svg document
svgDocument: this.paper.svg
});
// Create a new attrs with same structure as the model attrs { text: { *textAttributes* }}
var attrs = joint.util.setByPath({}, '.content', textAttrs, '/');
// Replace text attribute with the one we just processed.
attrs['.content'].text = text;
// Update the view using renderingOnlyAttributes parameter.
joint.dia.ElementView.prototype.update.call(this, cell, attrs);
}
});
joint.routers.manhattan = (function(g, _) {
'use strict';
var config = {
// size of the step to find a route
step: 10,
// use of the perpendicular linkView option to connect center of element with first vertex
perpendicular: true,
// should be source or target not to be consider as an obstacle
excludeEnds: [], // 'source', 'target'
// should be any element with a certain type not to be consider as an obstacle
excludeTypes: ['basic.Text'],
// if number of route finding loops exceed the maximum, stops searching and returns
// fallback route
maximumLoops: 2000,
// possible starting directions from an element
startDirections: ['left', 'right', 'top', 'bottom'],
// possible ending directions to an element
endDirections: ['left', 'right', 'top', 'bottom'],
// specify directions above
directionMap: {
right: { x: 1, y: 0 },
bottom: { x: 0, y: 1 },
left: { x: -1, y: 0 },
top: { x: 0, y: -1 }
},
// maximum change of the direction
maxAllowedDirectionChange: 90,
// padding applied on the element bounding boxes
paddingBox: function() {
var step = this.step;
return {
x: -step,
y: -step,
width: 2 * step,
height: 2 * step
};
},
// an array of directions to find next points on the route
directions: function() {
var step = this.step;
return [
{ offsetX: step , offsetY: 0 , cost: step },
{ offsetX: 0 , offsetY: step , cost: step },
{ offsetX: -step , offsetY: 0 , cost: step },
{ offsetX: 0 , offsetY: -step , cost: step }
];
},
// a penalty received for direction change
penalties: function() {
return {
0: 0,
45: this.step / 2,
90: this.step / 2
};
},
// a simple route used in situations, when main routing method fails
// (exceed loops, inaccessible).
fallbackRoute: function(from, to, opts) {
// Find an orthogonal route ignoring obstacles.
var point = ((opts.previousDirAngle || 0) % 180 === 0)
? g.point(from.x, to.y)
: g.point(to.x, from.y);
return [point, to];
},
// if a function is provided, it's used to route the link while dragging an end
// i.e. function(from, to, opts) { return []; }
draggingRoute: null
};
// Map of obstacles
// Helper structure to identify whether a point lies in an obstacle.
function ObstacleMap(opt) {
this.map = {};
this.options = opt;
// tells how to divide the paper when creating the elements map
this.mapGridSize = 100;
}
ObstacleMap.prototype.build = function(graph, link) {
var opt = this.options;
// source or target element could be excluded from set of obstacles
var excludedEnds = _.chain(opt.excludeEnds)
.map(link.get, link)
.pluck('id')
.map(graph.getCell, graph).value();
// Exclude any embedded elements from the source and the target element.
var excludedAncestors = [];
var source = graph.getCell(link.get('source').id);
if (source) {
excludedAncestors = _.union(excludedAncestors, _.map(source.getAncestors(), 'id'));
};
var target = graph.getCell(link.get('target').id);
if (target) {
excludedAncestors = _.union(excludedAncestors, _.map(target.getAncestors(), 'id'));
}
// builds a map of all elements for quicker obstacle queries (i.e. is a point contained
// in any obstacle?) (a simplified grid search)
// The paper is divided to smaller cells, where each of them holds an information which
// elements belong to it. When we query whether a point is in an obstacle we don't need
// to go through all obstacles, we check only those in a particular cell.
var mapGridSize = this.mapGridSize;
_.chain(graph.getElements())
// remove source and target element if required
.difference(excludedEnds)
// remove all elements whose type is listed in excludedTypes array
.reject(function(element) {
// reject any element which is an ancestor of either source or target
return _.contains(opt.excludeTypes, element.get('type')) || _.contains(excludedAncestors, element.id);
})
// change elements (models) to their bounding boxes
.invoke('getBBox')
// expand their boxes by specific padding
.invoke('moveAndExpand', opt.paddingBox)
// build the map
.foldl(function(map, bbox) {
var origin = bbox.origin().snapToGrid(mapGridSize);
var corner = bbox.corner().snapToGrid(mapGridSize);
for (var x = origin.x; x <= corner.x; x += mapGridSize) {
for (var y = origin.y; y <= corner.y; y += mapGridSize) {
var gridKey = x + '@' + y;
map[gridKey] = map[gridKey] || [];
map[gridKey].push(bbox);
}
}
return map;
}, this.map).value();
return this;
};
ObstacleMap.prototype.isPointAccessible = function(point) {
var mapKey = point.clone().snapToGrid(this.mapGridSize).toString();
return _.every(this.map[mapKey], function(obstacle) {
return !obstacle.containsPoint(point);
});
};
// Sorted Set
// Set of items sorted by given value.
function SortedSet() {
this.items = [];
this.hash = {};
this.values = {};
this.OPEN = 1;
this.CLOSE = 2;
}
SortedSet.prototype.add = function(item, value) {
if (this.hash[item]) {
// item removal
this.items.splice(this.items.indexOf(item), 1);
} else {
this.hash[item] = this.OPEN;
}
this.values[item] = value;
var index = _.sortedIndex(this.items, item, function(i) {
return this.values[i];
}, this);
this.items.splice(index, 0, item);
};
SortedSet.prototype.remove = function(item) {
this.hash[item] = this.CLOSE;
};
SortedSet.prototype.isOpen = function(item) {
return this.hash[item] === this.OPEN;
};
SortedSet.prototype.isClose = function(item) {
return this.hash[item] === this.CLOSE;
};
SortedSet.prototype.isEmpty = function() {
return this.items.length === 0;
};
SortedSet.prototype.pop = function() {
var item = this.items.shift();
this.remove(item);
return item;
};
// reconstructs a route by concating points with their parents
function reconstructRoute(parents, point) {
var route = [];
var prevDiff = { x: 0, y: 0 };
var current = point;
var parent;
while ((parent = parents[current])) {
var diff = parent.difference(current);
if (!diff.equals(prevDiff)) {
route.unshift(current);
prevDiff = diff;
}
current = parent;
}
route.unshift(current);
return route;
}
// find points around the rectangle taking given directions in the account
function getRectPoints(bbox, directionList, opt) {
var step = opt.step;
var center = bbox.center();
var startPoints = _.chain(opt.directionMap).pick(directionList).map(function(direction) {
var x = direction.x * bbox.width / 2;
var y = direction.y * bbox.height / 2;
var point = center.clone().offset(x, y);
if (bbox.containsPoint(point)) {
point.offset(direction.x * step, direction.y * step);
}
return point.snapToGrid(step);
}).value();
return startPoints;
};
// returns a direction index from start point to end point
function getDirectionAngle(start, end, dirLen) {
var q = 360 / dirLen;
return Math.floor(g.normalizeAngle(start.theta(end) + q / 2) / q) * q;
}
function getDirectionChange(angle1, angle2) {
var dirChange = Math.abs(angle1 - angle2);
return dirChange > 180 ? 360 - dirChange : dirChange;
}
// heurestic method to determine the distance between two points
function estimateCost(from, endPoints) {
var min = Infinity;
for (var i = 0, len = endPoints.length; i < len; i++) {
var cost = from.manhattanDistance(endPoints[i]);
if (cost < min) min = cost;
};
return min;
}
// finds the route between to points/rectangles implementing A* alghoritm
function findRoute(start, end, map, opt) {
var step = opt.step;
var startPoints, endPoints;
var startCenter, endCenter;
// set of points we start pathfinding from
if (start instanceof g.rect) {
startPoints = getRectPoints(start, opt.startDirections, opt);
startCenter = start.center();
} else {
startCenter = start.clone().snapToGrid(step);
startPoints = [startCenter];
}
// set of points we want the pathfinding to finish at
if (end instanceof g.rect) {
endPoints = getRectPoints(end, opt.endDirections, opt);
endCenter = end.center();
} else {
endCenter = end.clone().snapToGrid(step);
endPoints = [endCenter];
}
// take into account only accessible end points
startPoints = _.filter(startPoints, map.isPointAccessible, map);
endPoints = _.filter(endPoints, map.isPointAccessible, map);
// Check if there is a accessible end point.
// We would have to use a fallback route otherwise.
if (startPoints.length > 0 && endPoints.length > 0) {
// The set of tentative points to be evaluated, initially containing the start points.
var openSet = new SortedSet();
// Keeps reference to a point that is immediate predecessor of given element.
var parents = {};
// Cost from start to a point along best known path.
var costs = {};
_.each(startPoints, function(point) {
var key = point.toString();
openSet.add(key, estimateCost(point, endPoints));
costs[key] = 0;
});
// directions
var dir, dirChange;
var dirs = opt.directions;
var dirLen = dirs.length;
var loopsRemain = opt.maximumLoops;
var endPointsKeys = _.invoke(endPoints, 'toString');
// main route finding loop
while (!openSet.isEmpty() && loopsRemain > 0) {
// remove current from the open list
var currentKey = openSet.pop();
var currentPoint = g.point(currentKey);
var currentDist = costs[currentKey];
var previousDirAngle = currentDirAngle;
var currentDirAngle = parents[currentKey]
? getDirectionAngle(parents[currentKey], currentPoint, dirLen)
: opt.previousDirAngle != null ? opt.previousDirAngle : getDirectionAngle(startCenter, currentPoint, dirLen);
// Check if we reached any endpoint
if (endPointsKeys.indexOf(currentKey) >= 0) {
// We don't want to allow route to enter the end point in opposite direction.
dirChange = getDirectionChange(currentDirAngle, getDirectionAngle(currentPoint, endCenter, dirLen));
if (currentPoint.equals(endCenter) || dirChange < 180) {
opt.previousDirAngle = currentDirAngle;
return reconstructRoute(parents, currentPoint);
}
}
// Go over all possible directions and find neighbors.
for (var i = 0; i < dirLen; i++) {
dir = dirs[i];
dirChange = getDirectionChange(currentDirAngle, dir.angle);
// if the direction changed rapidly don't use this point
if (dirChange > opt.maxAllowedDirectionChange) {
continue;
}
var neighborPoint = currentPoint.clone().offset(dir.offsetX, dir.offsetY);
var neighborKey = neighborPoint.toString();
// Closed points from the openSet were already evaluated.
if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) {
continue;
}
// The current direction is ok to proccess.
var costFromStart = currentDist + dir.cost + opt.penalties[dirChange];
if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) {
// neighbor point has not been processed yet or the cost of the path
// from start is lesser than previously calcluated.
parents[neighborKey] = currentPoint;
costs[neighborKey] = costFromStart;
openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints));
};
};
loopsRemain--;
}
}
// no route found ('to' point wasn't either accessible or finding route took
// way to much calculations)
return opt.fallbackRoute(startCenter, endCenter, opt);
}
// resolve some of the options
function resolveOptions(opt) {
opt.directions = _.result(opt, 'directions');
opt.penalties = _.result(opt, 'penalties');
opt.paddingBox = _.result(opt, 'paddingBox');
_.each(opt.directions, function(direction) {
var point1 = new g.point(0, 0);
var point2 = new g.point(direction.offsetX, direction.offsetY);
var angle = g.normalizeAngle(point1.theta(point2));
direction.angle = angle;
});
}
// initiation of the route finding
function router(vertices, opt) {
resolveOptions(opt);
// enable/disable linkView perpendicular option
this.options.perpendicular = !!opt.perpendicular;
// expand boxes by specific padding
var sourceBBox = g.rect(this.sourceBBox).moveAndExpand(opt.paddingBox);
var targetBBox = g.rect(this.targetBBox).moveAndExpand(opt.paddingBox);
// pathfinding
var map = (new ObstacleMap(opt)).build(this.paper.model, this.model);
var oldVertices = _.map(vertices, g.point);
var newVertices = [];
var tailPoint = sourceBBox.center().snapToGrid(opt.step);
// find a route by concating all partial routes (routes need to go through the vertices)
// startElement -> vertex[1] -> ... -> vertex[n] -> endElement
for (var i = 0, len = oldVertices.length; i <= len; i++) {
var partialRoute = null;
var from = to || sourceBBox;
var to = oldVertices[i];
if (!to) {
to = targetBBox;
// 'to' is not a vertex. If the target is a point (i.e. it's not an element), we
// might use dragging route instead of main routing method if that is enabled.
var endingAtPoint = !this.model.get('source').id || !this.model.get('target').id;
if (endingAtPoint && _.isFunction(opt.draggingRoute)) {
// Make sure we passing points only (not rects).
var dragFrom = from instanceof g.rect ? from.center() : from;
partialRoute = opt.draggingRoute(dragFrom, to.origin(), opt);
}
}
// if partial route has not been calculated yet use the main routing method to find one
partialRoute = partialRoute || findRoute(from, to, map, opt);
var leadPoint = _.first(partialRoute);
if (leadPoint && leadPoint.equals(tailPoint)) {
// remove the first point if the previous partial route had the same point as last
partialRoute.shift();
}
tailPoint = _.last(partialRoute) || tailPoint;
Array.prototype.push.apply(newVertices, partialRoute);
};
return newVertices;
}
// public function
return function(vertices, opt, linkView) {
return router.call(linkView, vertices, _.extend({}, config, opt));
};
})(g, _);
joint.routers.metro = (function() {
if (!_.isFunction(joint.routers.manhattan)) {
throw('Metro requires the manhattan router.');
}
var config = {
// cost of a diagonal step (calculated if not defined).
diagonalCost: null,
// an array of directions to find next points on the route
directions: function() {
var step = this.step;
var diagonalCost = this.diagonalCost || Math.ceil(Math.sqrt(step * step << 1));
return [
{ offsetX: step , offsetY: 0 , cost: step },
{ offsetX: step , offsetY: step , cost: diagonalCost },
{ offsetX: 0 , offsetY: step , cost: step },
{ offsetX: -step , offsetY: step , cost: diagonalCost },
{ offsetX: -step , offsetY: 0 , cost: step },
{ offsetX: -step , offsetY: -step , cost: diagonalCost },
{ offsetX: 0 , offsetY: -step , cost: step },
{ offsetX: step , offsetY: -step , cost: diagonalCost }
];
},
maxAllowedDirectionChange: 45,
// a simple route used in situations, when main routing method fails
// (exceed loops, inaccessible).
fallbackRoute: function(from, to, opts) {
// Find a route which breaks by 45 degrees ignoring all obstacles.
var theta = from.theta(to);
var a = { x: to.x, y: from.y };
var b = { x: from.x, y: to.y };
if (theta % 180 > 90) {
var t = a;
a = b;
b = t;
}
var p1 = (theta % 90) < 45 ? a : b;
var l1 = g.line(from, p1);
var alpha = 90 * Math.ceil(theta / 90);
var p2 = g.point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1);
var l2 = g.line(to, p2);
var point = l1.intersection(l2);
return point ? [point.round(), to] : [to];
}
};
// public function
return function(vertices, opts, linkView) {
return joint.routers.manhattan(vertices, _.extend({}, config, opts), linkView);
};
})();
// Does not make any changes to vertices.
// Returns the arguments that are passed to it, unchanged.
joint.routers.normal = function(vertices, opt, linkView) {
return vertices;
};
// Routes the link always to/from a certain side
//
// Arguments:
// padding ... gap between the element and the first vertex. :: Default 40.
// side ... 'left' | 'right' | 'top' | 'bottom' :: Default 'bottom'.
//
joint.routers.oneSide = function(vertices, opt, linkView) {
var side = opt.side || 'bottom';
var padding = opt.padding || 40;
// LinkView contains cached source an target bboxes.
// Note that those are Geometry rectangle objects.
var sourceBBox = linkView.sourceBBox;
var targetBBox = linkView.targetBBox;
var sourcePoint = sourceBBox.center();
var targetPoint = targetBBox.center();
var coordinate, coordinateValue, dimension, direction;
switch (side) {
case 'bottom':
direction = 1;
coordinate = 'y';
dimension = 'height';
break;
case 'top':
direction = -1;
coordinate = 'y';
dimension = 'height';
break;
case 'left':
direction = -1;
coordinate = 'x';
dimension = 'width';
break;
case 'right':
direction = 1;
coordinate = 'x';
dimension = 'width';
break;
default:
throw new Error('Router: invalid side');
}
// move the points from the center of the element to outside of it.
sourcePoint[coordinate] += direction * (sourceBBox[dimension] / 2 + padding);
targetPoint[coordinate] += direction * (targetBBox[dimension] / 2 + padding);
// make link orthogonal (at least the first and last vertex).
if (direction * (sourcePoint[coordinate] - targetPoint[coordinate]) > 0) {
targetPoint[coordinate] = sourcePoint[coordinate];
} else {
sourcePoint[coordinate] = targetPoint[coordinate];
}
return [sourcePoint].concat(vertices, targetPoint);
};
joint.routers.orthogonal = (function() {
// bearing -> opposite bearing
var opposite = {
N: 'S',
S: 'N',
E: 'W',
W: 'E'
};
// bearing -> radians
var radians = {
N: -Math.PI / 2 * 3,
S: -Math.PI / 2,
E: 0,
W: Math.PI
};
// HELPERS //
// simple bearing method (calculates only orthogonal cardinals)
function bearing(from, to) {
if (from.x == to.x) return from.y > to.y ? 'N' : 'S';
if (from.y == to.y) return from.x > to.x ? 'W' : 'E';
return null;
}
// returns either width or height of a bbox based on the given bearing
function boxSize(bbox, brng) {
return bbox[brng == 'W' || brng == 'E' ? 'width' : 'height'];
}
// expands a box by specific value
function expand(bbox, val) {
return g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val });
}
// transform point to a rect
function pointBox(p) {
return g.rect(p.x, p.y, 0, 0);
}
// returns a minimal rect which covers the given boxes
function boundary(bbox1, bbox2) {
var x1 = Math.min(bbox1.x, bbox2.x);
var y1 = Math.min(bbox1.y, bbox2.y);
var x2 = Math.max(bbox1.x + bbox1.width, bbox2.x + bbox2.width);
var y2 = Math.max(bbox1.y + bbox1.height, bbox2.y + bbox2.height);
return g.rect(x1, y1, x2 - x1, y2 - y1);
}
// returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained
// in the given box
function freeJoin(p1, p2, bbox) {
var p = g.point(p1.x, p2.y);
if (bbox.containsPoint(p)) p = g.point(p2.x, p1.y);
// kept for reference
// if (bbox.containsPoint(p)) p = null;
return p;
}
// PARTIAL ROUTERS //
function vertexVertex(from, to, brng) {
var p1 = g.point(from.x, to.y);
var p2 = g.point(to.x, from.y);
var d1 = bearing(from, p1);
var d2 = bearing(from, p2);
var xBrng = opposite[brng];
var p = (d1 == brng || (d1 != xBrng && (d2 == xBrng || d2 != brng))) ? p1 : p2;
return { points: [p], direction: bearing(p, to) };
}
function elementVertex(from, to, fromBBox) {
var p = freeJoin(from, to, fromBBox);
return { points: [p], direction: bearing(p, to) };
}
function vertexElement(from, to, toBBox, brng) {
var route = {};
var pts = [g.point(from.x, to.y), g.point(to.x, from.y)];
var freePts = _.filter(pts, function(pt) { return !toBBox.containsPoint(pt); });
var freeBrngPts = _.filter(freePts, function(pt) { return bearing(pt, from) != brng; });
var p;
if (freeBrngPts.length > 0) {
// try to pick a point which bears the same direction as the previous segment
p = _.filter(freeBrngPts, function(pt) { return bearing(from, pt) == brng; }).pop();
p = p || freeBrngPts[0];
route.points = [p];
route.direction = bearing(p, to);
} else {
// Here we found only points which are either contained in the element or they would create
// a link segment going in opposite direction from the previous one.
// We take the point inside element and move it outside the element in the direction the
// route is going. Now we can join this point with the current end (using freeJoin).
p = _.difference(pts, freePts)[0];
var p2 = g.point(to).move(p, -boxSize(toBBox, brng) / 2);
var p1 = freeJoin(p2, from, toBBox);
route.points = [p1, p2];
route.direction = bearing(p2, to);
}
return route;
}
function elementElement(from, to, fromBBox, toBBox) {
var route = elementVertex(to, from, toBBox);
var p1 = route.points[0];
if (fromBBox.containsPoint(p1)) {
route = elementVertex(from, to, fromBBox);
var p2 = route.points[0];
if (toBBox.containsPoint(p2)) {
var fromBorder = g.point(from).move(p2, -boxSize(fromBBox, bearing(from, p2)) / 2);
var toBorder = g.point(to).move(p1, -boxSize(toBBox, bearing(to, p1)) / 2);
var mid = g.line(fromBorder, toBorder).midpoint();
var startRoute = elementVertex(from, mid, fromBBox);
var endRoute = vertexVertex(mid, to, startRoute.direction);
route.points = [startRoute.points[0], endRoute.points[0]];
route.direction = endRoute.direction;
}
}
return route;
}
// Finds route for situations where one of end is inside the other.
// Typically the route is conduct outside the outer element first and
// let go back to the inner element.
function insideElement(from, to, fromBBox, toBBox, brng) {
var route = {};
var bndry = expand(boundary(fromBBox, toBBox), 1);
// start from the point which is closer to the boundary
var reversed = bndry.center().distance(to) > bndry.center().distance(from);
var start = reversed ? to : from;
var end = reversed ? from : to;
var p1, p2, p3;
if (brng) {
// Points on circle with radius equals 'W + H` are always outside the rectangle
// with width W and height H if the center of that circle is the center of that rectangle.
p1 = g.point.fromPolar(bndry.width + bndry.height, radians[brng], start);
p1 = bndry.pointNearestToPoint(p1).move(p1, -1);
} else {
p1 = bndry.pointNearestToPoint(start).move(start, 1);
}
p2 = freeJoin(p1, end, bndry);
if (p1.round().equals(p2.round())) {
p2 = g.point.fromPolar(bndry.width + bndry.height, g.toRad(p1.theta(start)) + Math.PI / 2, end);
p2 = bndry.pointNearestToPoint(p2).move(end, 1).round();
p3 = freeJoin(p1, p2, bndry);
route.points = reversed ? [p2, p3, p1] : [p1, p3, p2];
} else {
route.points = reversed ? [p2, p1] : [p1, p2];
}
route.direction = reversed ? bearing(p1, to) : bearing(p2, to);
return route;
}
// MAIN ROUTER //
// Return points that one needs to draw a connection through in order to have a orthogonal link
// routing from source to target going through `vertices`.
function findOrthogonalRoute(vertices, opt, linkView) {
var padding = opt.elementPadding || 20;
var orthogonalVertices = [];
var sourceBBox = expand(linkView.sourceBBox, padding);
var targetBBox = expand(linkView.targetBBox, padding);
vertices = _.map(vertices, g.point);
vertices.unshift(sourceBBox.center());
vertices.push(targetBBox.center());
var brng;
for (var i = 0, max = vertices.length - 1; i < max; i++) {
var route = null;
var from = vertices[i];
var to = vertices[i + 1];
var isOrthogonal = !!bearing(from, to);
if (i == 0) {
if (i + 1 == max) { // route source -> target
// Expand one of elements by 1px so we detect also situations when they
// are positioned one next other with no gap between.
if (sourceBBox.intersect(expand(targetBBox, 1))) {
route = insideElement(from, to, sourceBBox, targetBBox);
} else if (!isOrthogonal) {
route = elementElement(from, to, sourceBBox, targetBBox);
}
} else { // route source -> vertex
if (sourceBBox.containsPoint(to)) {
route = insideElement(from, to, sourceBBox, expand(pointBox(to), padding));
} else if (!isOrthogonal) {
route = elementVertex(from, to, sourceBBox);
}
}
} else if (i + 1 == max) { // route vertex -> target
var orthogonalLoop = isOrthogonal && bearing(to, from) == brng;
if (targetBBox.containsPoint(from) || orthogonalLoop) {
route = insideElement(from, to, expand(pointBox(from), padding), targetBBox, brng);
} else if (!isOrthogonal) {
route = vertexElement(from, to, targetBBox, brng);
}
} else if (!isOrthogonal) { // route vertex -> vertex
route = vertexVertex(from, to, brng);
}
if (route) {
Array.prototype.push.apply(orthogonalVertices, route.points);
brng = route.direction;
} else {
// orthogonal route and not looped
brng = bearing(from, to);
}
if (i + 1 < max) {
orthogonalVertices.push(to);
}
}
return orthogonalVertices;
};
return findOrthogonalRoute;
})();
joint.connectors.normal = function(sourcePoint, targetPoint, vertices) {
// Construct the `d` attribute of the `` element.
var d = ['M', sourcePoint.x, sourcePoint.y];
_.each(vertices, function(vertex) {
d.push(vertex.x, vertex.y);
});
d.push(targetPoint.x, targetPoint.y);
return d.join(' ');
};
joint.connectors.rounded = function(sourcePoint, targetPoint, vertices, opts) {
opts = opts || {};
var offset = opts.radius || 10;
var c1, c2, d1, d2, prev, next;
// Construct the `d` attribute of the `` element.
var d = ['M', sourcePoint.x, sourcePoint.y];
_.each(vertices, function(vertex, index) {
// the closest vertices
prev = vertices[index - 1] || sourcePoint;
next = vertices[index + 1] || targetPoint;
// a half distance to the closest vertex
d1 = d2 || g.point(vertex).distance(prev) / 2;
d2 = g.point(vertex).distance(next) / 2;
// control points
c1 = g.point(vertex).move(prev, -Math.min(offset, d1)).round();
c2 = g.point(vertex).move(next, -Math.min(offset, d2)).round();
d.push(c1.x, c1.y, 'S', vertex.x, vertex.y, c2.x, c2.y, 'L');
});
d.push(targetPoint.x, targetPoint.y);
return d.join(' ');
};
joint.connectors.smooth = function(sourcePoint, targetPoint, vertices) {
var d;
if (vertices.length) {
d = g.bezier.curveThroughPoints([sourcePoint].concat(vertices).concat([targetPoint]));
} else {
// if we have no vertices use a default cubic bezier curve, cubic bezier requires
// two control points. The two control points are both defined with X as mid way
// between the source and target points. SourceControlPoint Y is equal to sourcePoint Y
// and targetControlPointY being equal to targetPointY. Handle situation were
// sourcePointX is greater or less then targetPointX.
var controlPointX = (sourcePoint.x < targetPoint.x)
? targetPoint.x - ((targetPoint.x - sourcePoint.x) / 2)
: sourcePoint.x - ((sourcePoint.x - targetPoint.x) / 2);
d = [
'M', sourcePoint.x, sourcePoint.y,
'C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y,
targetPoint.x, targetPoint.y
];
}
return d.join(' ');
};
joint.connectors.jumpover = (function(_, g) {
// default size of jump if not specified in options
var JUMP_SIZE = 5;
// available jump types
var JUMP_TYPES = ['arc', 'gap', 'cubic'];
// takes care of math. error for case when jump is too close to end of line
var CLOSE_PROXIMITY_PADDING = 1;
// list of connector types not to jump over.
var IGNORED_CONNECTORS = ['smooth'];
/**
* Transform start/end and vertices into series of lines
* @param {g.point} sourcePoint start point
* @param {g.point} targetPoint end point
* @param {g.point[]} vertices optional list of vertices
* @return {g.line[]} [description]
*/
function createLines(sourcePoint, targetPoint, vertices) {
// make a flattened array of all points
var points = [].concat(sourcePoint, vertices, targetPoint);
return points.reduce(function(resultLines, point, idx) {
// if there is a next point, make a line with it
var nextPoint = points[idx + 1];
if (nextPoint != null) {
resultLines[idx] = g.line(point, nextPoint);
}
return resultLines;
}, []);
}
function setupUpdating(jumpOverLinkView) {
var updateList = jumpOverLinkView.paper._jumpOverUpdateList;
// first time setup for this paper
if (updateList == null) {
updateList = jumpOverLinkView.paper._jumpOverUpdateList = [];
jumpOverLinkView.paper.on('cell:pointerup', updateJumpOver);
jumpOverLinkView.paper.model.on('reset', function() {
updateList = [];
});
}
// add this link to a list so it can be updated when some other link is updated
if (updateList.indexOf(jumpOverLinkView) < 0) {
updateList.push(jumpOverLinkView);
// watch for change of connector type or removal of link itself
// to remove the link from a list of jump over connectors
jumpOverLinkView.listenToOnce(jumpOverLinkView.model, 'change:connector remove', function() {
updateList.splice(updateList.indexOf(jumpOverLinkView), 1);
});
}
}
/**
* Handler for a batch:stop event to force
* update of all registered links with jump over connector
* @param {object} batchEvent optional object with info about batch
*/
function updateJumpOver() {
var updateList = this._jumpOverUpdateList;
for (var i = 0; i < updateList.length; i++) {
updateList[i].update();
}
}
/**
* Utility function to collect all intersection poinst of a single
* line against group of other lines.
* @param {g.line} line where to find points
* @param {g.line[]} crossCheckLines lines to cross
* @return {g.point[]} list of intersection points
*/
function findLineIntersections(line, crossCheckLines) {
return _(crossCheckLines).map(function(crossCheckLine) {
return line.intersection(crossCheckLine);
}).compact().value();
}
/**
* Sorting function for list of points by their distance.
* @param {g.point} p1 first point
* @param {g.point} p2 second point
* @return {number} squared distance between points
*/
function sortPoints(p1, p2) {
return g.line(p1, p2).squaredLength();
}
/**
* Split input line into multiple based on intersection points.
* @param {g.line} line input line to split
* @param {g.point[]} intersections poinst where to split the line
* @param {number} jumpSize the size of jump arc (length empty spot on a line)
* @return {g.line[]} list of lines being split
*/
function createJumps(line, intersections, jumpSize) {
return intersections.reduce(function(resultLines, point, idx) {
// skipping points that were merged with the previous line
// to make bigger arc over multiple lines that are close to each other
if (point.skip === true) {
return resultLines;
}
// always grab the last line from buffer and modify it
var lastLine = resultLines.pop() || line;
// calculate start and end of jump by moving by a given size of jump
var jumpStart = g.point(point).move(lastLine.start, -(jumpSize));
var jumpEnd = g.point(point).move(lastLine.start, +(jumpSize));
// now try to look at the next intersection point
var nextPoint = intersections[idx + 1];
if (nextPoint != null) {
var distance = jumpEnd.distance(nextPoint);
if (distance <= jumpSize) {
// next point is close enough, move the jump end by this
// difference and mark the next point to be skipped
jumpEnd = nextPoint.move(lastLine.start, distance);
nextPoint.skip = true;
}
} else {
// this block is inside of `else` as an optimization so the distance is
// not calculated when we know there are no other intersection points
var endDistance = jumpStart.distance(lastLine.end);
// if the end is too close to possible jump, draw remaining line instead of a jump
if (endDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) {
resultLines.push(lastLine);
return resultLines;
}
}
var startDistance = jumpEnd.distance(lastLine.start);
if (startDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) {
// if the start of line is too close to jump, draw that line instead of a jump
resultLines.push(lastLine);
return resultLines;
}
// finally create a jump line
var jumpLine = g.line(jumpStart, jumpEnd);
// it's just simple line but with a `isJump` property
jumpLine.isJump = true;
resultLines.push(
g.line(lastLine.start, jumpStart),
jumpLine,
g.line(jumpEnd, lastLine.end)
);
return resultLines;
}, []);
}
/**
* Assemble `D` attribute of a SVG path by iterating given lines.
* @param {g.line[]} lines source lines to use
* @param {number} jumpSize the size of jump arc (length empty spot on a line)
* @return {string}
*/
function buildPath(lines, jumpSize, jumpType) {
// first move to the start of a first line
var start = ['M', lines[0].start.x, lines[0].start.y];
// make a paths from lines
var paths = _(lines).map(function(line) {
if (line.isJump) {
var diff;
if (jumpType === 'arc') {
diff = line.start.difference(line.end);
// determine rotation of arc based on difference between points
var xAxisRotate = Number(diff.x < 0 && diff.y < 0);
// for a jump line we create an arc instead
return ['A', jumpSize, jumpSize, 0, 0, xAxisRotate, line.end.x, line.end.y];
} else if (jumpType === 'gap') {
return ['M', line.end.x, line.end.y];
} else if (jumpType === 'cubic') {
diff = line.start.difference(line.end);
var angle = line.start.theta(line.end);
var xOffset = jumpSize * 0.6;
var yOffset = jumpSize * 1.35;
// determine rotation of curve based on difference between points
if (diff.x < 0 && diff.y < 0) {
yOffset *= -1;
}
var controlStartPoint = g.point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle);
var controlEndPoint = g.point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle);
// create a cubic bezier curve
return ['C', controlStartPoint.x, controlStartPoint.y, controlEndPoint.x, controlEndPoint.y, line.end.x, line.end.y];
}
}
return ['L', line.end.x, line.end.y];
}).flatten().value();
return [].concat(start, paths).join(' ');
}
/**
* Actual connector function that will be run on every update.
* @param {g.point} sourcePoint start point of this link
* @param {g.point} targetPoint end point of this link
* @param {g.point[]} vertices of this link
* @param {object} opts options
* @property {number} size optional size of a jump arc
* @return {string} created `D` attribute of SVG path
*/
return function(sourcePoint, targetPoint, vertices, opts) { // eslint-disable-line max-params
setupUpdating(this);
var jumpSize = opts.size || JUMP_SIZE;
var jumpType = opts.jump && ('' + opts.jump).toLowerCase();
var ignoreConnectors = opts.ignoreConnectors || IGNORED_CONNECTORS;
// grab the first jump type as a default if specified one is invalid
if (JUMP_TYPES.indexOf(jumpType) === -1) {
jumpType = JUMP_TYPES[0];
}
var paper = this.paper;
var graph = paper.model;
var allLinks = graph.getLinks();
// there is just one link, draw it directly
if (allLinks.length === 1) {
return buildPath(
createLines(sourcePoint, targetPoint, vertices),
jumpSize, jumpType
);
}
var thisModel = this.model;
var thisIndex = allLinks.indexOf(thisModel);
var defaultConnector = paper.options.defaultConnector || {};
// not all links are meant to be jumped over.
var links = allLinks.filter(function(link, idx) {
var connector = link.get('connector') || defaultConnector;
// avoid jumping over links with connector type listed in `ignored connectors`.
if (_.contains(ignoreConnectors, connector.name)) {
return false;
}
// filter out links that are above this one and have the same connector type
// otherwise there would double hoops for each intersection
if (idx > thisIndex) {
return connector.name !== 'jumpover';
}
return true;
});
// find views for all links
var linkViews = links.map(function(link) {
return paper.findViewByModel(link);
});
// create lines for this link
var thisLines = createLines(
sourcePoint,
targetPoint,
vertices
);
// create lines for all other links
var linkLines = linkViews.map(function(linkView) {
if (linkView == null) {
return [];
}
if (linkView === this) {
return thisLines;
}
return createLines(
linkView.sourcePoint,
linkView.targetPoint,
linkView.route
);
}, this);
// transform lines for this link by splitting with jump lines at
// points of intersection with other links
var jumpingLines = thisLines.reduce(function(resultLines, thisLine) {
// iterate all links and grab the intersections with this line
// these are then sorted by distance so the line can be split more easily
var intersections = _(links).map(function(link, i) {
// don't intersection with itself
if (link === thisModel) {
return null;
}
return findLineIntersections(thisLine, linkLines[i]);
}).flatten().compact().sortBy(_.partial(sortPoints, thisLine.start)).value();
if (intersections.length > 0) {
// split the line based on found intersection points
resultLines.push.apply(resultLines, createJumps(thisLine, intersections, jumpSize));
} else {
// without any intersection the line goes uninterrupted
resultLines.push(thisLine);
}
return resultLines;
}, []);
return buildPath(jumpingLines, jumpSize, jumpType);
};
}(_, g));
joint.highlighters.addClass = {
className: 'highlighted',
highlight: function(cellView, magnetEl, opt) {
var className = opt.className || this.className;
V(magnetEl).addClass(className);
},
unhighlight: function(cellView, magnetEl, opt) {
var className = opt.className || this.className;
V(magnetEl).removeClass(className);
}
};
joint.highlighters.opacity = {
highlight: function(cellView, magnetEl, opt) {
V(magnetEl).addClass('joint-highlight-opacity');
},
unhighlight: function(cellView, magnetEl, opt) {
V(magnetEl).removeClass('joint-highlight-opacity');
}
};
joint.highlighters.stroke = {
defaultOptions: {
padding: 3,
rx: 0,
ry: 0
},
_views: {},
highlight: function(cellView, magnetEl, opt) {
// Only highlight once.
if (this._views[magnetEl.id]) return;
opt = _.defaults(opt || {}, this.defaultOptions);
var magnetVel = V(magnetEl);
var magnetBBox = magnetVel.bbox(true/* without transforms */);
try {
var pathData = magnetVel.convertToPathData();
} catch (error) {
// Failed to get path data from magnet element.
// Draw a rectangle around the entire cell view instead.
pathData = V.rectToPath(_.extend({}, opt, magnetBBox));
}
var highlightVel = V('path').attr({
d: pathData,
'class': 'joint-highlight-stroke',
'pointer-events': 'none'
});
highlightVel.transform(cellView.el.getCTM().inverse());
highlightVel.transform(magnetEl.getCTM());
var padding = opt.padding;
if (padding) {
// Add padding to the highlight element.
var cx = magnetBBox.x + (magnetBBox.width / 2);
var cy = magnetBBox.y + (magnetBBox.height / 2);
var sx = (magnetBBox.width + padding) / magnetBBox.width;
var sy = (magnetBBox.height + padding) / magnetBBox.height;
highlightVel.transform({
a: sx,
b: 0,
c: 0,
d: sy,
e: cx - sx * cx,
f: cy - sy * cy
});
}
// This will handle the joint-theme-* class name for us.
var highlightView = this._views[magnetEl.id] = new joint.mvc.View({
// This is necessary because we're passing in a vectorizer element (not jQuery).
el: highlightVel.node,
$el: highlightVel
});
// Remove the highlight view when the cell is removed from the graph.
highlightView.listenTo(cellView.model, 'remove', highlightView.remove);
cellView.vel.append(highlightVel);
},
unhighlight: function(cellView, magnetEl, opt) {
opt = _.defaults(opt || {}, this.defaultOptions);
if (this._views[magnetEl.id]) {
this._views[magnetEl.id].remove();
this._views[magnetEl.id] = null;
}
}
};
joint.g = g;
joint.V = joint.Vectorizer = V;
return joint;
}));