--- /dev/null
+/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.5.5
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the code for creating and manipulating anchors.
+ *
+ * Copyright (c) 2010 - 2013 Simon Porritt (simon.porritt@gmail.com)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+;(function() {
+
+ //
+ // manages anchors for all elements.
+ //
+ jsPlumb.AnchorManager = function(params) {
+ var _amEndpoints = {},
+ continuousAnchors = {},
+ continuousAnchorLocations = {},
+ userDefinedContinuousAnchorLocations = {},
+ continuousAnchorOrientations = {},
+ Orientation = { HORIZONTAL : "horizontal", VERTICAL : "vertical", DIAGONAL : "diagonal", IDENTITY:"identity" },
+ connectionsByElementId = {},
+ self = this,
+ anchorLists = {},
+ jsPlumbInstance = params.jsPlumbInstance,
+ jpcl = jsPlumb.CurrentLibrary,
+ floatingConnections = {},
+ // TODO this functions uses a crude method of determining orientation between two elements.
+ // 'diagonal' should be chosen when the angle of the line between the two centers is around
+ // one of 45, 135, 225 and 315 degrees. maybe +- 15 degrees.
+ // used by AnchorManager.redraw
+ calculateOrientation = function(sourceId, targetId, sd, td, sourceAnchor, targetAnchor) {
+
+ if (sourceId === targetId) return {
+ orientation:Orientation.IDENTITY,
+ a:["top", "top"]
+ };
+
+ var theta = Math.atan2((td.centery - sd.centery) , (td.centerx - sd.centerx)),
+ theta2 = Math.atan2((sd.centery - td.centery) , (sd.centerx - td.centerx)),
+ h = ((sd.left <= td.left && sd.right >= td.left) || (sd.left <= td.right && sd.right >= td.right) ||
+ (sd.left <= td.left && sd.right >= td.right) || (td.left <= sd.left && td.right >= sd.right)),
+ v = ((sd.top <= td.top && sd.bottom >= td.top) || (sd.top <= td.bottom && sd.bottom >= td.bottom) ||
+ (sd.top <= td.top && sd.bottom >= td.bottom) || (td.top <= sd.top && td.bottom >= sd.bottom)),
+ possiblyTranslateEdges = function(edges) {
+ // this function checks to see if either anchor is Continuous, and if so, runs the suggested edge
+ // through the anchor: Continuous anchors can say which faces they support, and they get to choose
+ // whether a certain face is honoured, or, if not, which face to replace it with. the behaviour when
+ // choosing an alternate face is to try for the opposite face first, then the next one clockwise, and then
+ // the opposite of that one.
+ return [
+ sourceAnchor.isContinuous ? sourceAnchor.verifyEdge(edges[0]) : edges[0],
+ targetAnchor.isContinuous ? targetAnchor.verifyEdge(edges[1]) : edges[1]
+ ];
+ },
+ out = {
+ orientation:Orientation.DIAGONAL,
+ theta:theta,
+ theta2:theta2
+ };
+
+ if (! (h || v)) {
+ if (td.left > sd.left && td.top > sd.top)
+ out.a = ["right", "top"];
+ else if (td.left > sd.left && sd.top > td.top)
+ out.a = [ "top", "left"];
+ else if (td.left < sd.left && td.top < sd.top)
+ out.a = [ "top", "right"];
+ else if (td.left < sd.left && td.top > sd.top)
+ out.a = ["left", "top" ];
+ }
+ else if (h) {
+ out.orientation = Orientation.HORIZONTAL;
+ out.a = sd.top < td.top ? ["bottom", "top"] : ["top", "bottom"];
+ }
+ else {
+ out.orientation = Orientation.VERTICAL;
+ out.a = sd.left < td.left ? ["right", "left"] : ["left", "right"];
+ }
+
+ out.a = possiblyTranslateEdges(out.a);
+ return out;
+ },
+ // used by placeAnchors function
+ placeAnchorsOnLine = function(desc, elementDimensions, elementPosition,
+ connections, horizontal, otherMultiplier, reverse) {
+ var a = [], step = elementDimensions[horizontal ? 0 : 1] / (connections.length + 1);
+
+ for (var i = 0; i < connections.length; i++) {
+ var val = (i + 1) * step, other = otherMultiplier * elementDimensions[horizontal ? 1 : 0];
+ if (reverse)
+ val = elementDimensions[horizontal ? 0 : 1] - val;
+
+ var dx = (horizontal ? val : other), x = elementPosition[0] + dx, xp = dx / elementDimensions[0],
+ dy = (horizontal ? other : val), y = elementPosition[1] + dy, yp = dy / elementDimensions[1];
+
+ a.push([ x, y, xp, yp, connections[i][1], connections[i][2] ]);
+ }
+
+ return a;
+ },
+ // used by edgeSortFunctions
+ currySort = function(reverseAngles) {
+ return function(a,b) {
+ var r = true;
+ if (reverseAngles) {
+ /*if (a[0][0] < b[0][0])
+ r = true;
+ else
+ r = a[0][1] > b[0][1];*/
+ r = a[0][0] < b[0][0];
+ }
+ else {
+ /*if (a[0][0] > b[0][0])
+ r= true;
+ else
+ r =a[0][1] > b[0][1];
+ */
+ r = a[0][0] > b[0][0];
+ }
+ return r === false ? -1 : 1;
+ };
+ },
+ // used by edgeSortFunctions
+ leftSort = function(a,b) {
+ // first get adjusted values
+ var p1 = a[0][0] < 0 ? -Math.PI - a[0][0] : Math.PI - a[0][0],
+ p2 = b[0][0] < 0 ? -Math.PI - b[0][0] : Math.PI - b[0][0];
+ if (p1 > p2) return 1;
+ else return a[0][1] > b[0][1] ? 1 : -1;
+ },
+ // used by placeAnchors
+ edgeSortFunctions = {
+ "top":function(a, b) { return a[0] > b[0] ? 1 : -1; },
+ "right":currySort(true),
+ "bottom":currySort(true),
+ "left":leftSort
+ },
+ // used by placeAnchors
+ _sortHelper = function(_array, _fn) { return _array.sort(_fn); },
+ // used by AnchorManager.redraw
+ placeAnchors = function(elementId, _anchorLists) {
+ var cd = jsPlumbInstance.getCachedData(elementId), sS = cd.s, sO = cd.o,
+ placeSomeAnchors = function(desc, elementDimensions, elementPosition, unsortedConnections, isHorizontal, otherMultiplier, orientation) {
+ if (unsortedConnections.length > 0) {
+ var sc = _sortHelper(unsortedConnections, edgeSortFunctions[desc]), // puts them in order based on the target element's pos on screen
+ reverse = desc === "right" || desc === "top",
+ anchors = placeAnchorsOnLine(desc, elementDimensions,
+ elementPosition, sc,
+ isHorizontal, otherMultiplier, reverse );
+
+ // takes a computed anchor position and adjusts it for parent offset and scroll, then stores it.
+ var _setAnchorLocation = function(endpoint, anchorPos) {
+ var a = jsPlumbInstance.adjustForParentOffsetAndScroll([anchorPos[0], anchorPos[1]], endpoint.canvas);
+ continuousAnchorLocations[endpoint.id] = [ a[0], a[1], anchorPos[2], anchorPos[3] ];
+ continuousAnchorOrientations[endpoint.id] = orientation;
+ };
+
+ for (var i = 0; i < anchors.length; i++) {
+ var c = anchors[i][4], weAreSource = c.endpoints[0].elementId === elementId, weAreTarget = c.endpoints[1].elementId === elementId;
+ if (weAreSource)
+ _setAnchorLocation(c.endpoints[0], anchors[i]);
+ else if (weAreTarget)
+ _setAnchorLocation(c.endpoints[1], anchors[i]);
+ }
+ }
+ };
+
+ placeSomeAnchors("bottom", sS, [sO.left,sO.top], _anchorLists.bottom, true, 1, [0,1]);
+ placeSomeAnchors("top", sS, [sO.left,sO.top], _anchorLists.top, true, 0, [0,-1]);
+ placeSomeAnchors("left", sS, [sO.left,sO.top], _anchorLists.left, false, 0, [-1,0]);
+ placeSomeAnchors("right", sS, [sO.left,sO.top], _anchorLists.right, false, 1, [1,0]);
+ };
+
+ this.reset = function() {
+ _amEndpoints = {};
+ connectionsByElementId = {};
+ anchorLists = {};
+ };
+ this.addFloatingConnection = function(key, conn) {
+ floatingConnections[key] = conn;
+ };
+ this.removeFloatingConnection = function(key) {
+ delete floatingConnections[key];
+ };
+ this.newConnection = function(conn) {
+ var sourceId = conn.sourceId, targetId = conn.targetId,
+ ep = conn.endpoints,
+ doRegisterTarget = true,
+ registerConnection = function(otherIndex, otherEndpoint, otherAnchor, elId, c) {
+ if ((sourceId == targetId) && otherAnchor.isContinuous){
+ // remove the target endpoint's canvas. we dont need it.
+ jpcl.removeElement(ep[1].canvas);
+ doRegisterTarget = false;
+ }
+ jsPlumbUtil.addToList(connectionsByElementId, elId, [c, otherEndpoint, otherAnchor.constructor == jsPlumb.DynamicAnchor]);
+ };
+
+ registerConnection(0, ep[0], ep[0].anchor, targetId, conn);
+ if (doRegisterTarget)
+ registerConnection(1, ep[1], ep[1].anchor, sourceId, conn);
+ };
+ var removeEndpointFromAnchorLists = function(endpoint) {
+ (function(list, eId) {
+ if (list) { // transient anchors dont get entries in this list.
+ var f = function(e) { return e[4] == eId; };
+ jsPlumbUtil.removeWithFunction(list.top, f);
+ jsPlumbUtil.removeWithFunction(list.left, f);
+ jsPlumbUtil.removeWithFunction(list.bottom, f);
+ jsPlumbUtil.removeWithFunction(list.right, f);
+ }
+ })(anchorLists[endpoint.elementId], endpoint.id);
+ };
+ this.connectionDetached = function(connInfo) {
+ var connection = connInfo.connection || connInfo,
+ sourceId = connInfo.sourceId,
+ targetId = connInfo.targetId,
+ ep = connection.endpoints,
+ removeConnection = function(otherIndex, otherEndpoint, otherAnchor, elId, c) {
+ if (otherAnchor != null && otherAnchor.constructor == jsPlumb.FloatingAnchor) {
+ // no-op
+ }
+ else {
+ jsPlumbUtil.removeWithFunction(connectionsByElementId[elId], function(_c) {
+ return _c[0].id == c.id;
+ });
+ }
+ };
+
+ removeConnection(1, ep[1], ep[1].anchor, sourceId, connection);
+ removeConnection(0, ep[0], ep[0].anchor, targetId, connection);
+
+ // remove from anchorLists
+ removeEndpointFromAnchorLists(connection.endpoints[0]);
+ removeEndpointFromAnchorLists(connection.endpoints[1]);
+
+ self.redraw(connection.sourceId);
+ self.redraw(connection.targetId);
+ };
+ this.add = function(endpoint, elementId) {
+ jsPlumbUtil.addToList(_amEndpoints, elementId, endpoint);
+ };
+ this.changeId = function(oldId, newId) {
+ connectionsByElementId[newId] = connectionsByElementId[oldId];
+ _amEndpoints[newId] = _amEndpoints[oldId];
+ delete connectionsByElementId[oldId];
+ delete _amEndpoints[oldId];
+ };
+ this.getConnectionsFor = function(elementId) {
+ return connectionsByElementId[elementId] || [];
+ };
+ this.getEndpointsFor = function(elementId) {
+ return _amEndpoints[elementId] || [];
+ };
+ this.deleteEndpoint = function(endpoint) {
+ jsPlumbUtil.removeWithFunction(_amEndpoints[endpoint.elementId], function(e) {
+ return e.id == endpoint.id;
+ });
+ removeEndpointFromAnchorLists(endpoint);
+ };
+ this.clearFor = function(elementId) {
+ delete _amEndpoints[elementId];
+ _amEndpoints[elementId] = [];
+ };
+ // updates the given anchor list by either updating an existing anchor's info, or adding it. this function
+ // also removes the anchor from its previous list, if the edge it is on has changed.
+ // all connections found along the way (those that are connected to one of the faces this function
+ // operates on) are added to the connsToPaint list, as are their endpoints. in this way we know to repaint
+ // them wthout having to calculate anything else about them.
+ var _updateAnchorList = function(lists, theta, order, conn, aBoolean, otherElId, idx, reverse, edgeId, elId, connsToPaint, endpointsToPaint) {
+ // first try to find the exact match, but keep track of the first index of a matching element id along the way.s
+ var exactIdx = -1,
+ firstMatchingElIdx = -1,
+ endpoint = conn.endpoints[idx],
+ endpointId = endpoint.id,
+ oIdx = [1,0][idx],
+ values = [ [ theta, order ], conn, aBoolean, otherElId, endpointId ],
+ listToAddTo = lists[edgeId],
+ listToRemoveFrom = endpoint._continuousAnchorEdge ? lists[endpoint._continuousAnchorEdge] : null;
+
+ if (listToRemoveFrom) {
+ var rIdx = jsPlumbUtil.findWithFunction(listToRemoveFrom, function(e) { return e[4] == endpointId; });
+ if (rIdx != -1) {
+ listToRemoveFrom.splice(rIdx, 1);
+ // get all connections from this list
+ for (var i = 0; i < listToRemoveFrom.length; i++) {
+ jsPlumbUtil.addWithFunction(connsToPaint, listToRemoveFrom[i][1], function(c) { return c.id == listToRemoveFrom[i][1].id; });
+ jsPlumbUtil.addWithFunction(endpointsToPaint, listToRemoveFrom[i][1].endpoints[idx], function(e) { return e.id == listToRemoveFrom[i][1].endpoints[idx].id; });
+ jsPlumbUtil.addWithFunction(endpointsToPaint, listToRemoveFrom[i][1].endpoints[oIdx], function(e) { return e.id == listToRemoveFrom[i][1].endpoints[oIdx].id; });
+ }
+ }
+ }
+
+ for (i = 0; i < listToAddTo.length; i++) {
+ if (params.idx == 1 && listToAddTo[i][3] === otherElId && firstMatchingElIdx == -1)
+ firstMatchingElIdx = i;
+ jsPlumbUtil.addWithFunction(connsToPaint, listToAddTo[i][1], function(c) { return c.id == listToAddTo[i][1].id; });
+ jsPlumbUtil.addWithFunction(endpointsToPaint, listToAddTo[i][1].endpoints[idx], function(e) { return e.id == listToAddTo[i][1].endpoints[idx].id; });
+ jsPlumbUtil.addWithFunction(endpointsToPaint, listToAddTo[i][1].endpoints[oIdx], function(e) { return e.id == listToAddTo[i][1].endpoints[oIdx].id; });
+ }
+ if (exactIdx != -1) {
+ listToAddTo[exactIdx] = values;
+ }
+ else {
+ var insertIdx = reverse ? firstMatchingElIdx != -1 ? firstMatchingElIdx : 0 : listToAddTo.length; // of course we will get this from having looked through the array shortly.
+ listToAddTo.splice(insertIdx, 0, values);
+ }
+
+ // store this for next time.
+ endpoint._continuousAnchorEdge = edgeId;
+ };
+
+ //
+ // find the entry in an endpoint's list for this connection and update its target endpoint
+ // with the current target in the connection.
+ //
+ //
+ this.updateOtherEndpoint = function(elId, oldTargetId, newTargetId, connection) {
+ var sIndex = jsPlumbUtil.findWithFunction(connectionsByElementId[elId], function(i) {
+ return i[0].id === connection.id;
+ }),
+ tIndex = jsPlumbUtil.findWithFunction(connectionsByElementId[oldTargetId], function(i) {
+ return i[0].id === connection.id;
+ });
+
+ // update or add data for source
+ if (sIndex != -1) {
+ connectionsByElementId[elId][sIndex][0] = connection;
+ connectionsByElementId[elId][sIndex][1] = connection.endpoints[1];
+ connectionsByElementId[elId][sIndex][2] = connection.endpoints[1].anchor.constructor == jsPlumb.DynamicAnchor;
+ }
+
+ // remove entry for previous target (if there)
+ if (tIndex > -1) {
+
+ connectionsByElementId[oldTargetId].splice(tIndex, 1);
+ // add entry for new target
+ jsPlumbUtil.addToList(connectionsByElementId, newTargetId, [connection, connection.endpoints[0], connection.endpoints[0].anchor.constructor == jsPlumb.DynamicAnchor]);
+ }
+ };
+
+ //
+ // notification that the connection given has changed source from the originalId to the newId.
+ // This involves:
+ // 1. removing the connection from the list of connections stored for the originalId
+ // 2. updating the source information for the target of the connection
+ // 3. re-registering the connection in connectionsByElementId with the newId
+ //
+ this.sourceChanged = function(originalId, newId, connection) {
+ // remove the entry that points from the old source to the target
+ jsPlumbUtil.removeWithFunction(connectionsByElementId[originalId], function(info) {
+ return info[0].id === connection.id;
+ });
+ // find entry for target and update it
+ var tIdx = jsPlumbUtil.findWithFunction(connectionsByElementId[connection.targetId], function(i) {
+ return i[0].id === connection.id;
+ });
+ if (tIdx > -1) {
+ connectionsByElementId[connection.targetId][tIdx][0] = connection;
+ connectionsByElementId[connection.targetId][tIdx][1] = connection.endpoints[0];
+ connectionsByElementId[connection.targetId][tIdx][2] = connection.endpoints[0].anchor.constructor == jsPlumb.DynamicAnchor;
+ }
+ // add entry for new source
+ jsPlumbUtil.addToList(connectionsByElementId, newId, [connection, connection.endpoints[1], connection.endpoints[1].anchor.constructor == jsPlumb.DynamicAnchor]);
+ };
+
+ //
+ // moves the given endpoint from `currentId` to `element`.
+ // This involves:
+ //
+ // 1. changing the key in _amEndpoints under which the endpoint is stored
+ // 2. changing the source or target values in all of the endpoint's connections
+ // 3. changing the array in connectionsByElementId in which the endpoint's connections
+ // are stored (done by either sourceChanged or updateOtherEndpoint)
+ //
+ this.rehomeEndpoint = function(ep, currentId, element) {
+ var eps = _amEndpoints[currentId] || [],
+ elementId = jsPlumbInstance.getId(element);
+
+ if (elementId !== currentId) {
+ var idx = jsPlumbUtil.indexOf(eps, ep);
+ if (idx > -1) {
+ var _ep = eps.splice(idx, 1)[0];
+ self.add(_ep, elementId);
+ }
+ }
+
+ for (var i = 0; i < ep.connections.length; i++) {
+ if (ep.connections[i].sourceId == currentId) {
+ ep.connections[i].sourceId = ep.elementId;
+ ep.connections[i].source = ep.element;
+ self.sourceChanged(currentId, ep.elementId, ep.connections[i]);
+ }
+ else if(ep.connections[i].targetId == currentId) {
+ ep.connections[i].targetId = ep.elementId;
+ ep.connections[i].target = ep.element;
+ self.updateOtherEndpoint(ep.connections[i].sourceId, currentId, ep.elementId, ep.connections[i]);
+ }
+ }
+ };
+
+ this.redraw = function(elementId, ui, timestamp, offsetToUI, clearEdits, doNotRecalcEndpoint) {
+
+ if (!jsPlumbInstance.isSuspendDrawing()) {
+ // get all the endpoints for this element
+ var ep = _amEndpoints[elementId] || [],
+ endpointConnections = connectionsByElementId[elementId] || [],
+ connectionsToPaint = [],
+ endpointsToPaint = [],
+ anchorsToUpdate = [];
+
+ timestamp = timestamp || jsPlumbInstance.timestamp();
+ // offsetToUI are values that would have been calculated in the dragManager when registering
+ // an endpoint for an element that had a parent (somewhere in the hierarchy) that had been
+ // registered as draggable.
+ offsetToUI = offsetToUI || {left:0, top:0};
+ if (ui) {
+ ui = {
+ left:ui.left + offsetToUI.left,
+ top:ui.top + offsetToUI.top
+ };
+ }
+
+ // valid for one paint cycle.
+ var myOffset = jsPlumbInstance.updateOffset( { elId : elementId, offset : ui, recalc : false, timestamp : timestamp }),
+ orientationCache = {};
+
+ // actually, first we should compute the orientation of this element to all other elements to which
+ // this element is connected with a continuous anchor (whether both ends of the connection have
+ // a continuous anchor or just one)
+
+ for (var i = 0; i < endpointConnections.length; i++) {
+ var conn = endpointConnections[i][0],
+ sourceId = conn.sourceId,
+ targetId = conn.targetId,
+ sourceContinuous = conn.endpoints[0].anchor.isContinuous,
+ targetContinuous = conn.endpoints[1].anchor.isContinuous;
+
+ if (sourceContinuous || targetContinuous) {
+ var oKey = sourceId + "_" + targetId,
+ oKey2 = targetId + "_" + sourceId,
+ o = orientationCache[oKey],
+ oIdx = conn.sourceId == elementId ? 1 : 0;
+
+ if (sourceContinuous && !anchorLists[sourceId]) anchorLists[sourceId] = { top:[], right:[], bottom:[], left:[] };
+ if (targetContinuous && !anchorLists[targetId]) anchorLists[targetId] = { top:[], right:[], bottom:[], left:[] };
+
+ if (elementId != targetId) jsPlumbInstance.updateOffset( { elId : targetId, timestamp : timestamp });
+ if (elementId != sourceId) jsPlumbInstance.updateOffset( { elId : sourceId, timestamp : timestamp });
+
+ var td = jsPlumbInstance.getCachedData(targetId),
+ sd = jsPlumbInstance.getCachedData(sourceId);
+
+ if (targetId == sourceId && (sourceContinuous || targetContinuous)) {
+ // here we may want to improve this by somehow determining the face we'd like
+ // to put the connector on. ideally, when drawing, the face should be calculated
+ // by determining which face is closest to the point at which the mouse button
+ // was released. for now, we're putting it on the top face.
+ _updateAnchorList(
+ anchorLists[sourceId],
+ -Math.PI / 2,
+ 0,
+ conn,
+ false,
+ targetId,
+ 0, false, "top", sourceId, connectionsToPaint, endpointsToPaint);
+ }
+ else {
+ if (!o) {
+ o = calculateOrientation(sourceId, targetId, sd.o, td.o, conn.endpoints[0].anchor, conn.endpoints[1].anchor);
+ orientationCache[oKey] = o;
+ // this would be a performance enhancement, but the computed angles need to be clamped to
+ //the (-PI/2 -> PI/2) range in order for the sorting to work properly.
+ /* orientationCache[oKey2] = {
+ orientation:o.orientation,
+ a:[o.a[1], o.a[0]],
+ theta:o.theta + Math.PI,
+ theta2:o.theta2 + Math.PI
+ };*/
+ }
+ if (sourceContinuous) _updateAnchorList(anchorLists[sourceId], o.theta, 0, conn, false, targetId, 0, false, o.a[0], sourceId, connectionsToPaint, endpointsToPaint);
+ if (targetContinuous) _updateAnchorList(anchorLists[targetId], o.theta2, -1, conn, true, sourceId, 1, true, o.a[1], targetId, connectionsToPaint, endpointsToPaint);
+ }
+
+ if (sourceContinuous) jsPlumbUtil.addWithFunction(anchorsToUpdate, sourceId, function(a) { return a === sourceId; });
+ if (targetContinuous) jsPlumbUtil.addWithFunction(anchorsToUpdate, targetId, function(a) { return a === targetId; });
+ jsPlumbUtil.addWithFunction(connectionsToPaint, conn, function(c) { return c.id == conn.id; });
+ if ((sourceContinuous && oIdx === 0) || (targetContinuous && oIdx === 1))
+ jsPlumbUtil.addWithFunction(endpointsToPaint, conn.endpoints[oIdx], function(e) { return e.id == conn.endpoints[oIdx].id; });
+ }
+ }
+ // place Endpoints whose anchors are continuous but have no Connections
+ for (i = 0; i < ep.length; i++) {
+ if (ep[i].connections.length === 0 && ep[i].anchor.isContinuous) {
+ if (!anchorLists[elementId]) anchorLists[elementId] = { top:[], right:[], bottom:[], left:[] };
+ _updateAnchorList(anchorLists[elementId], -Math.PI / 2, 0, {endpoints:[ep[i], ep[i]], paint:function(){}}, false, elementId, 0, false, "top", elementId, connectionsToPaint, endpointsToPaint);
+ jsPlumbUtil.addWithFunction(anchorsToUpdate, elementId, function(a) { return a === elementId; });
+ }
+ }
+ // now place all the continuous anchors we need to;
+ for (i = 0; i < anchorsToUpdate.length; i++) {
+ placeAnchors(anchorsToUpdate[i], anchorLists[anchorsToUpdate[i]]);
+ }
+
+ // now that continuous anchors have been placed, paint all the endpoints for this element
+ // TODO performance: add the endpoint ids to a temp array, and then when iterating in the next
+ // loop, check that we didn't just paint that endpoint. we can probably shave off a few more milliseconds this way.
+ for (i = 0; i < ep.length; i++) {
+ ep[i].paint( { timestamp : timestamp, offset : myOffset, dimensions : myOffset.s, recalc:doNotRecalcEndpoint !== true });
+ }
+ // ... and any other endpoints we came across as a result of the continuous anchors.
+ for (i = 0; i < endpointsToPaint.length; i++) {
+ var cd = jsPlumbInstance.getCachedData(endpointsToPaint[i].elementId);
+ // dont use timestamp for this endpoint, as it is not for the current element and we may
+ // have needed to recalculate anchor position due to the element for the endpoint moving.
+ //endpointsToPaint[i].paint( { timestamp : null, offset : cd, dimensions : cd.s });
+
+ endpointsToPaint[i].paint( { timestamp : timestamp, offset : cd, dimensions : cd.s });
+ }
+
+ // paint all the standard and "dynamic connections", which are connections whose other anchor is
+ // static and therefore does need to be recomputed; we make sure that happens only one time.
+
+ // TODO we could have compiled a list of these in the first pass through connections; might save some time.
+ for (i = 0; i < endpointConnections.length; i++) {
+ var otherEndpoint = endpointConnections[i][1];
+ if (otherEndpoint.anchor.constructor == jsPlumb.DynamicAnchor) {
+ otherEndpoint.paint({ elementWithPrecedence:elementId, timestamp:timestamp });
+ jsPlumbUtil.addWithFunction(connectionsToPaint, endpointConnections[i][0], function(c) { return c.id == endpointConnections[i][0].id; });
+ // all the connections for the other endpoint now need to be repainted
+ for (var k = 0; k < otherEndpoint.connections.length; k++) {
+ if (otherEndpoint.connections[k] !== endpointConnections[i][0])
+ jsPlumbUtil.addWithFunction(connectionsToPaint, otherEndpoint.connections[k], function(c) { return c.id == otherEndpoint.connections[k].id; });
+ }
+ } else if (otherEndpoint.anchor.constructor == jsPlumb.Anchor) {
+ jsPlumbUtil.addWithFunction(connectionsToPaint, endpointConnections[i][0], function(c) { return c.id == endpointConnections[i][0].id; });
+ }
+ }
+ // paint current floating connection for this element, if there is one.
+ var fc = floatingConnections[elementId];
+ if (fc)
+ fc.paint({timestamp:timestamp, recalc:false, elId:elementId});
+
+ // paint all the connections
+ for (i = 0; i < connectionsToPaint.length; i++) {
+ // if not a connection between the two elements in question dont use the timestamp.
+ var ts =timestamp;// ((connectionsToPaint[i].sourceId == sourceId && connectionsToPaint[i].targetId == targetId) ||
+ //(connectionsToPaint[i].sourceId == targetId && connectionsToPaint[i].targetId == sourceId)) ? timestamp : null;
+ connectionsToPaint[i].paint({elId:elementId, timestamp:ts, recalc:false, clearEdits:clearEdits});
+ }
+ }
+ };
+
+ var ContinuousAnchor = function(anchorParams) {
+ jsPlumbUtil.EventGenerator.apply(this);
+ this.type = "Continuous";
+ this.isDynamic = true;
+ this.isContinuous = true;
+ var faces = anchorParams.faces || ["top", "right", "bottom", "left"],
+ clockwise = !(anchorParams.clockwise === false),
+ availableFaces = { },
+ opposites = { "top":"bottom", "right":"left","left":"right","bottom":"top" },
+ clockwiseOptions = { "top":"right", "right":"bottom","left":"top","bottom":"left" },
+ antiClockwiseOptions = { "top":"left", "right":"top","left":"bottom","bottom":"right" },
+ secondBest = clockwise ? clockwiseOptions : antiClockwiseOptions,
+ lastChoice = clockwise ? antiClockwiseOptions : clockwiseOptions,
+ cssClass = anchorParams.cssClass || "";
+
+ for (var i = 0; i < faces.length; i++) { availableFaces[faces[i]] = true; }
+
+ // if the given edge is supported, returns it. otherwise looks for a substitute that _is_
+ // supported. if none supported we also return the request edge.
+ this.verifyEdge = function(edge) {
+ if (availableFaces[edge]) return edge;
+ else if (availableFaces[opposites[edge]]) return opposites[edge];
+ else if (availableFaces[secondBest[edge]]) return secondBest[edge];
+ else if (availableFaces[lastChoice[edge]]) return lastChoice[edge];
+ return edge; // we have to give them something.
+ };
+
+ this.compute = function(params) {
+ return userDefinedContinuousAnchorLocations[params.element.id] || continuousAnchorLocations[params.element.id] || [0,0];
+ };
+ this.getCurrentLocation = function(params) {
+ return userDefinedContinuousAnchorLocations[params.element.id] || continuousAnchorLocations[params.element.id] || [0,0];
+ };
+ this.getOrientation = function(endpoint) {
+ return continuousAnchorOrientations[endpoint.id] || [0,0];
+ };
+ this.clearUserDefinedLocation = function() {
+ delete userDefinedContinuousAnchorLocations[anchorParams.elementId];
+ };
+ this.setUserDefinedLocation = function(loc) {
+ userDefinedContinuousAnchorLocations[anchorParams.elementId] = loc;
+ };
+ this.getCssClass = function() { return cssClass; };
+ this.setCssClass = function(c) { cssClass = c; };
+ };
+
+ // continuous anchors
+ jsPlumbInstance.continuousAnchorFactory = {
+ get:function(params) {
+ var existing = continuousAnchors[params.elementId];
+ if (!existing) {
+ existing = new ContinuousAnchor(params);
+ continuousAnchors[params.elementId] = existing;
+ }
+ return existing;
+ },
+ clear:function(elementId) {
+ delete continuousAnchors[elementId];
+ }
+ };
+ };
+
+ /**
+ * Anchors model a position on some element at which an Endpoint may be located. They began as a first class citizen of jsPlumb, ie. a user
+ * was required to create these themselves, but over time this has been replaced by the concept of referring to them either by name (eg. "TopMiddle"),
+ * or by an array describing their coordinates (eg. [ 0, 0.5, 0, -1 ], which is the same as "TopMiddle"). jsPlumb now handles all of the
+ * creation of Anchors without user intervention.
+ */
+ jsPlumb.Anchor = function(params) {
+ this.x = params.x || 0;
+ this.y = params.y || 0;
+ this.elementId = params.elementId;
+ this.cssClass = params.cssClass || "";
+ this.userDefinedLocation = null;
+ this.orientation = params.orientation || [ 0, 0 ];
+
+ jsPlumbUtil.EventGenerator.apply(this);
+
+ var jsPlumbInstance = params.jsPlumbInstance;//,
+ //lastTimestamp = null;//, lastReturnValue = null;
+
+ this.lastReturnValue = null;
+ this.offsets = params.offsets || [ 0, 0 ];
+ this.timestamp = null;
+ this.compute = function(params) {
+
+ var xy = params.xy, wh = params.wh, element = params.element, timestamp = params.timestamp;
+
+ if(params.clearUserDefinedLocation)
+ this.userDefinedLocation = null;
+
+ if (timestamp && timestamp === self.timestamp)
+ return this.lastReturnValue;
+
+ if (this.userDefinedLocation != null) {
+ this.lastReturnValue = this.userDefinedLocation;
+ }
+ else {
+
+ this.lastReturnValue = [ xy[0] + (this.x * wh[0]) + this.offsets[0], xy[1] + (this.y * wh[1]) + this.offsets[1] ];
+ // adjust loc if there is an offsetParent
+ this.lastReturnValue = jsPlumbInstance.adjustForParentOffsetAndScroll(this.lastReturnValue, element.canvas);
+ }
+
+ this.timestamp = timestamp;
+ return this.lastReturnValue;
+ };
+
+ this.getCurrentLocation = function(params) {
+ return (this.lastReturnValue == null || (params.timestamp != null && this.timestamp != params.timestamp)) ? this.compute(params) : this.lastReturnValue;
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Anchor, jsPlumbUtil.EventGenerator, {
+ equals : function(anchor) {
+ if (!anchor) return false;
+ var ao = anchor.getOrientation(),
+ o = this.getOrientation();
+ return this.x == anchor.x && this.y == anchor.y && this.offsets[0] == anchor.offsets[0] && this.offsets[1] == anchor.offsets[1] && o[0] == ao[0] && o[1] == ao[1];
+ },
+ getUserDefinedLocation : function() {
+ return this.userDefinedLocation;
+ },
+ setUserDefinedLocation : function(l) {
+ this.userDefinedLocation = l;
+ },
+ clearUserDefinedLocation : function() {
+ this.userDefinedLocation = null;
+ },
+ getOrientation : function(_endpoint) { return this.orientation; },
+ getCssClass : function() { return this.cssClass; }
+ });
+
+ /**
+ * An Anchor that floats. its orientation is computed dynamically from
+ * its position relative to the anchor it is floating relative to. It is used when creating
+ * a connection through drag and drop.
+ *
+ * TODO FloatingAnchor could totally be refactored to extend Anchor just slightly.
+ */
+ jsPlumb.FloatingAnchor = function(params) {
+
+ jsPlumb.Anchor.apply(this, arguments);
+
+ // this is the anchor that this floating anchor is referenced to for
+ // purposes of calculating the orientation.
+ var ref = params.reference,
+ jpcl = jsPlumb.CurrentLibrary,
+ jsPlumbInstance = params.jsPlumbInstance,
+ // the canvas this refers to.
+ refCanvas = params.referenceCanvas,
+ size = jpcl.getSize(jpcl.getElementObject(refCanvas)),
+ // these are used to store the current relative position of our
+ // anchor wrt the reference anchor. they only indicate
+ // direction, so have a value of 1 or -1 (or, very rarely, 0). these
+ // values are written by the compute method, and read
+ // by the getOrientation method.
+ xDir = 0, yDir = 0,
+ // temporary member used to store an orientation when the floating
+ // anchor is hovering over another anchor.
+ orientation = null,
+ _lastResult = null;
+
+ // clear from parent. we want floating anchor orientation to always be computed.
+ this.orientation = null;
+
+ // set these to 0 each; they are used by certain types of connectors in the loopback case,
+ // when the connector is trying to clear the element it is on. but for floating anchor it's not
+ // very important.
+ this.x = 0; this.y = 0;
+
+ this.isFloating = true;
+
+ this.compute = function(params) {
+ var xy = params.xy, element = params.element,
+ result = [ xy[0] + (size[0] / 2), xy[1] + (size[1] / 2) ]; // return origin of the element. we may wish to improve this so that any object can be the drag proxy.
+
+ // adjust loc if there is an offsetParent
+ result = jsPlumbInstance.adjustForParentOffsetAndScroll(result, element.canvas);
+
+ _lastResult = result;
+ return result;
+ };
+
+ this.getOrientation = function(_endpoint) {
+ if (orientation) return orientation;
+ else {
+ var o = ref.getOrientation(_endpoint);
+ // here we take into account the orientation of the other
+ // anchor: if it declares zero for some direction, we declare zero too. this might not be the most awesome. perhaps we can come
+ // up with a better way. it's just so that the line we draw looks like it makes sense. maybe this wont make sense.
+ return [ Math.abs(o[0]) * xDir * -1,
+ Math.abs(o[1]) * yDir * -1 ];
+ }
+ };
+
+ /**
+ * notification the endpoint associated with this anchor is hovering
+ * over another anchor; we want to assume that anchor's orientation
+ * for the duration of the hover.
+ */
+ this.over = function(anchor, endpoint) {
+ orientation = anchor.getOrientation(endpoint);
+ };
+
+ /**
+ * notification the endpoint associated with this anchor is no
+ * longer hovering over another anchor; we should resume calculating
+ * orientation as we normally do.
+ */
+ this.out = function() { orientation = null; };
+
+ this.getCurrentLocation = function(params) { return _lastResult == null ? this.compute(params) : _lastResult; };
+ };
+ jsPlumbUtil.extend(jsPlumb.FloatingAnchor, jsPlumb.Anchor);
+
+ var _convertAnchor = function(anchor, jsPlumbInstance, elementId) {
+ return anchor.constructor == jsPlumb.Anchor ? anchor: jsPlumbInstance.makeAnchor(anchor, elementId, jsPlumbInstance);
+ };
+
+ /*
+ * A DynamicAnchor is an Anchor that contains a list of other Anchors, which it cycles
+ * through at compute time to find the one that is located closest to
+ * the center of the target element, and returns that Anchor's compute
+ * method result. this causes endpoints to follow each other with
+ * respect to the orientation of their target elements, which is a useful
+ * feature for some applications.
+ *
+ */
+ jsPlumb.DynamicAnchor = function(params) {
+ jsPlumb.Anchor.apply(this, arguments);
+
+ this.isSelective = true;
+ this.isDynamic = true;
+ this.anchors = [];
+ this.elementId = params.elementId;
+ this.jsPlumbInstance = params.jsPlumbInstance;
+
+ for (var i = 0; i < params.anchors.length; i++)
+ this.anchors[i] = _convertAnchor(params.anchors[i], this.jsPlumbInstance, this.elementId);
+ this.addAnchor = function(anchor) { this.anchors.push(_convertAnchor(anchor, this.jsPlumbInstance, this.elementId)); };
+ this.getAnchors = function() { return this.anchors; };
+ this.locked = false;
+ var _curAnchor = this.anchors.length > 0 ? this.anchors[0] : null,
+ _curIndex = this.anchors.length > 0 ? 0 : -1,
+ _lastAnchor = _curAnchor,
+ self = this,
+
+ // helper method to calculate the distance between the centers of the two elements.
+ _distance = function(anchor, cx, cy, xy, wh) {
+ var ax = xy[0] + (anchor.x * wh[0]), ay = xy[1] + (anchor.y * wh[1]),
+ acx = xy[0] + (wh[0] / 2), acy = xy[1] + (wh[1] / 2);
+ return (Math.sqrt(Math.pow(cx - ax, 2) + Math.pow(cy - ay, 2)) +
+ Math.sqrt(Math.pow(acx - ax, 2) + Math.pow(acy - ay, 2)));
+ },
+ // default method uses distance between element centers. you can provide your own method in the dynamic anchor
+ // constructor (and also to jsPlumb.makeDynamicAnchor). the arguments to it are four arrays:
+ // xy - xy loc of the anchor's element
+ // wh - anchor's element's dimensions
+ // txy - xy loc of the element of the other anchor in the connection
+ // twh - dimensions of the element of the other anchor in the connection.
+ // anchors - the list of selectable anchors
+ _anchorSelector = params.selector || function(xy, wh, txy, twh, anchors) {
+ var cx = txy[0] + (twh[0] / 2), cy = txy[1] + (twh[1] / 2);
+ var minIdx = -1, minDist = Infinity;
+ for ( var i = 0; i < anchors.length; i++) {
+ var d = _distance(anchors[i], cx, cy, xy, wh);
+ if (d < minDist) {
+ minIdx = i + 0;
+ minDist = d;
+ }
+ }
+ return anchors[minIdx];
+ };
+
+ this.compute = function(params) {
+ var xy = params.xy, wh = params.wh, timestamp = params.timestamp, txy = params.txy, twh = params.twh;
+
+ if(params.clearUserDefinedLocation)
+ userDefinedLocation = null;
+
+ this.timestamp = timestamp;
+
+ var udl = self.getUserDefinedLocation();
+ if (udl != null) {
+ return udl;
+ }
+
+ // if anchor is locked or an opposite element was not given, we
+ // maintain our state. anchor will be locked
+ // if it is the source of a drag and drop.
+ if (this.locked || txy == null || twh == null)
+ return _curAnchor.compute(params);
+ else
+ params.timestamp = null; // otherwise clear this, i think. we want the anchor to compute.
+
+ _curAnchor = _anchorSelector(xy, wh, txy, twh, this.anchors);
+ this.x = _curAnchor.x;
+ this.y = _curAnchor.y;
+
+ if (_curAnchor != _lastAnchor)
+ this.fire("anchorChanged", _curAnchor);
+
+ _lastAnchor = _curAnchor;
+
+ return _curAnchor.compute(params);
+ };
+
+ this.getCurrentLocation = function(params) {
+ return this.getUserDefinedLocation() || (_curAnchor != null ? _curAnchor.getCurrentLocation(params) : null);
+ };
+
+ this.getOrientation = function(_endpoint) { return _curAnchor != null ? _curAnchor.getOrientation(_endpoint) : [ 0, 0 ]; };
+ this.over = function(anchor, endpoint) { if (_curAnchor != null) _curAnchor.over(anchor, endpoint); };
+ this.out = function() { if (_curAnchor != null) _curAnchor.out(); };
+
+ this.getCssClass = function() { return (_curAnchor && _curAnchor.getCssClass()) || ""; };
+ };
+ jsPlumbUtil.extend(jsPlumb.DynamicAnchor, jsPlumb.Anchor);
+
+// -------- basic anchors ------------------
+ var _curryAnchor = function(x, y, ox, oy, type, fnInit) {
+ jsPlumb.Anchors[type] = function(params) {
+ var a = params.jsPlumbInstance.makeAnchor([ x, y, ox, oy, 0, 0 ], params.elementId, params.jsPlumbInstance);
+ a.type = type;
+ if (fnInit) fnInit(a, params);
+ return a;
+ };
+ };
+
+ _curryAnchor(0.5, 0, 0,-1, "TopCenter");
+ _curryAnchor(0.5, 1, 0, 1, "BottomCenter");
+ _curryAnchor(0, 0.5, -1, 0, "LeftMiddle");
+ _curryAnchor(1, 0.5, 1, 0, "RightMiddle");
+ // from 1.4.2: Top, Right, Bottom, Left
+ _curryAnchor(0.5, 0, 0,-1, "Top");
+ _curryAnchor(0.5, 1, 0, 1, "Bottom");
+ _curryAnchor(0, 0.5, -1, 0, "Left");
+ _curryAnchor(1, 0.5, 1, 0, "Right");
+ _curryAnchor(0.5, 0.5, 0, 0, "Center");
+ _curryAnchor(1, 0, 0,-1, "TopRight");
+ _curryAnchor(1, 1, 0, 1, "BottomRight");
+ _curryAnchor(0, 0, 0, -1, "TopLeft");
+ _curryAnchor(0, 1, 0, 1, "BottomLeft");
+
+// ------- dynamic anchors -------------------
+
+ // default dynamic anchors chooses from Top, Right, Bottom, Left
+ jsPlumb.Defaults.DynamicAnchors = function(params) {
+ return params.jsPlumbInstance.makeAnchors(["TopCenter", "RightMiddle", "BottomCenter", "LeftMiddle"], params.elementId, params.jsPlumbInstance);
+ };
+
+ // default dynamic anchors bound to name 'AutoDefault'
+ jsPlumb.Anchors.AutoDefault = function(params) {
+ var a = params.jsPlumbInstance.makeDynamicAnchor(jsPlumb.Defaults.DynamicAnchors(params));
+ a.type = "AutoDefault";
+ return a;
+ };
+
+// ------- continuous anchors -------------------
+
+ var _curryContinuousAnchor = function(type, faces) {
+ jsPlumb.Anchors[type] = function(params) {
+ var a = params.jsPlumbInstance.makeAnchor(["Continuous", { faces:faces }], params.elementId, params.jsPlumbInstance);
+ a.type = type;
+ return a;
+ };
+ };
+
+ jsPlumb.Anchors.Continuous = function(params) {
+ return params.jsPlumbInstance.continuousAnchorFactory.get(params);
+ };
+
+ _curryContinuousAnchor("ContinuousLeft", ["left"]);
+ _curryContinuousAnchor("ContinuousTop", ["top"]);
+ _curryContinuousAnchor("ContinuousBottom", ["bottom"]);
+ _curryContinuousAnchor("ContinuousRight", ["right"]);
+
+// ------- position assign anchors -------------------
+
+ // this anchor type lets you assign the position at connection time.
+ _curryAnchor(0, 0, 0, 0, "Assign", function(anchor, params) {
+ // find what to use as the "position finder". the user may have supplied a String which represents
+ // the id of a position finder in jsPlumb.AnchorPositionFinders, or the user may have supplied the
+ // position finder as a function. we find out what to use and then set it on the anchor.
+ var pf = params.position || "Fixed";
+ anchor.positionFinder = pf.constructor == String ? params.jsPlumbInstance.AnchorPositionFinders[pf] : pf;
+ // always set the constructor params; the position finder might need them later (the Grid one does,
+ // for example)
+ anchor.constructorParams = params;
+ });
+
+ // these are the default anchor positions finders, which are used by the makeTarget function. supplying
+ // a position finder argument to that function allows you to specify where the resulting anchor will
+ // be located
+ jsPlumbInstance.prototype.AnchorPositionFinders = {
+ "Fixed": function(dp, ep, es, params) {
+ return [ (dp.left - ep.left) / es[0], (dp.top - ep.top) / es[1] ];
+ },
+ "Grid":function(dp, ep, es, params) {
+ var dx = dp.left - ep.left, dy = dp.top - ep.top,
+ gx = es[0] / (params.grid[0]), gy = es[1] / (params.grid[1]),
+ mx = Math.floor(dx / gx), my = Math.floor(dy / gy);
+ return [ ((mx * gx) + (gx / 2)) / es[0], ((my * gy) + (gy / 2)) / es[1] ];
+ }
+ };
+
+// ------- perimeter anchors -------------------
+
+ jsPlumb.Anchors.Perimeter = function(params) {
+ params = params || {};
+ var anchorCount = params.anchorCount || 60,
+ shape = params.shape;
+
+ if (!shape) throw new Error("no shape supplied to Perimeter Anchor type");
+
+ var _circle = function() {
+ var r = 0.5, step = Math.PI * 2 / anchorCount, current = 0, a = [];
+ for (var i = 0; i < anchorCount; i++) {
+ var x = r + (r * Math.sin(current)),
+ y = r + (r * Math.cos(current));
+ a.push( [ x, y, 0, 0 ] );
+ current += step;
+ }
+ return a;
+ },
+ _path = function(segments) {
+ var anchorsPerFace = anchorCount / segments.length, a = [],
+ _computeFace = function(x1, y1, x2, y2, fractionalLength) {
+ anchorsPerFace = anchorCount * fractionalLength;
+ var dx = (x2 - x1) / anchorsPerFace, dy = (y2 - y1) / anchorsPerFace;
+ for (var i = 0; i < anchorsPerFace; i++) {
+ a.push( [
+ x1 + (dx * i),
+ y1 + (dy * i),
+ 0,
+ 0
+ ]);
+ }
+ };
+
+ for (var i = 0; i < segments.length; i++)
+ _computeFace.apply(null, segments[i]);
+
+ return a;
+ },
+ _shape = function(faces) {
+ var s = [];
+ for (var i = 0; i < faces.length; i++) {
+ s.push([faces[i][0], faces[i][1], faces[i][2], faces[i][3], 1 / faces.length]);
+ }
+ return _path(s);
+ },
+ _rectangle = function() {
+ return _shape([
+ [ 0, 0, 1, 0 ], [ 1, 0, 1, 1 ], [ 1, 1, 0, 1 ], [ 0, 1, 0, 0 ]
+ ]);
+ };
+
+ var _shapes = {
+ "Circle":_circle,
+ "Ellipse":_circle,
+ "Diamond":function() {
+ return _shape([
+ [ 0.5, 0, 1, 0.5 ], [ 1, 0.5, 0.5, 1 ], [ 0.5, 1, 0, 0.5 ], [ 0, 0.5, 0.5, 0 ]
+ ]);
+ },
+ "Rectangle":_rectangle,
+ "Square":_rectangle,
+ "Triangle":function() {
+ return _shape([
+ [ 0.5, 0, 1, 1 ], [ 1, 1, 0, 1 ], [ 0, 1, 0.5, 0]
+ ]);
+ },
+ "Path":function(params) {
+ var points = params.points, p = [], tl = 0;
+ for (var i = 0; i < points.length - 1; i++) {
+ var l = Math.sqrt(Math.pow(points[i][2] - points[i][0]) + Math.pow(points[i][3] - points[i][1]));
+ tl += l;
+ p.push([points[i][0], points[i][1], points[i+1][0], points[i+1][1], l]);
+ }
+ for (var j = 0; j < p.length; j++) {
+ p[j][4] = p[j][4] / tl;
+ }
+ return _path(p);
+ }
+ },
+ _rotate = function(points, amountInDegrees) {
+ var o = [], theta = amountInDegrees / 180 * Math.PI ;
+ for (var i = 0; i < points.length; i++) {
+ var _x = points[i][0] - 0.5,
+ _y = points[i][1] - 0.5;
+
+ o.push([
+ 0.5 + ((_x * Math.cos(theta)) - (_y * Math.sin(theta))),
+ 0.5 + ((_x * Math.sin(theta)) + (_y * Math.cos(theta))),
+ points[i][2],
+ points[i][3]
+ ]);
+ }
+ return o;
+ };
+
+ if (!_shapes[shape]) throw new Error("Shape [" + shape + "] is unknown by Perimeter Anchor type");
+
+ var da = _shapes[shape](params);
+ if (params.rotation) da = _rotate(da, params.rotation);
+ var a = params.jsPlumbInstance.makeDynamicAnchor(da);
+ a.type = "Perimeter";
+ return a;
+ };
+})();
\ No newline at end of file