webgme
Version:
Web-based Generic Modeling Environment
708 lines (596 loc) • 26.2 kB
JavaScript
/*globals DEBUG,define,WebGMEGlobal*/
/*jshint browser: true*/
/**
* @author rkereskenyi / https://github.com/rkereskenyi
*/
define([
'js/logger',
'module',
'AutoRouterActionApplier',
'./ConnectionRouteManager2',
'js/Utils/SaveToDisk'
], function (Logger,
module,
ActionApplier,
ConnectionRouteManager2,
Saver) {
'use strict';
var ConnectionRouteManager3,
DESIGNERITEM_SUBCOMPONENT_SEPARATOR = '_x_',
WORKER = true;
ConnectionRouteManager3 = function (options) {
if (window.Worker && WORKER) {
// A queue of item operations to perform after the item is created.
// Unfortunately, promises didn't work because they
// simply place the callbacks (eg, item move, item delete) on
// the event queue rather than executing the callback themselves
this._onItemCreateQueue = {};
this.workerQueue = [];
// TODO: If merging into one js file, this may break
var currentDir = module.id.split('/'),
workerFile;
currentDir.pop();
currentDir = currentDir.join('/');
workerFile = currentDir + '/AutoRouter.Worker.js';
this.worker = new Worker(workerFile);
this.worker.postMessage([WebGMEGlobal.gmeConfig.client]);
this.worker.onmessage = this._handleWorkerResponse.bind(this);
} else {
this.readyToDownload = false;
// inherit from the ActionApplier
window._.extend(this.prototype, ActionApplier.prototype);
}
this.init();
var loggerName = (options && options.loggerName) || 'gme:Widgets:DiagramDesigner:ConnectionRouteManager3';
this.logger = (options && options.logger) || Logger.create(loggerName, WebGMEGlobal.gmeConfig.client.log);
this.diagramDesigner = options ? options.diagramDesigner : null;
this.simpleRouter = new ConnectionRouteManager2(options);
if (this.diagramDesigner === undefined || this.diagramDesigner === null) {
this.logger.error('Trying to initialize a ConnectionRouteManager3 without a canvas...');
throw new Error('ConnectionRouteManager3 can not be created');
}
this.logger.debug('ConnectionRouteManager3 ctor finished');
this._portSeparator = DESIGNERITEM_SUBCOMPONENT_SEPARATOR;
};
// These next 2 methods are only used if a web worker is used; otherwise,
// they are overridden by ActionApplier
ConnectionRouteManager3.prototype.init = ActionApplier.prototype._clearRecords;
ConnectionRouteManager3.prototype._invokeAutoRouterMethod = function () {
var array = Array.prototype.slice.call(arguments), // Remove the extra 'arguments' stuff
id;
if (this.workerReady) {
this.worker.postMessage(array);
} else {
this.workerQueue.push(array);
}
// Update some record keeping
switch (array[0]) {
case 'addPath':
case 'addBox':
id = array[1][1];
this._onItemCreateQueue[id] = [];
break;
case 'clear':
this.init(); // Clear the records
break;
case 'remove':
id = array[1][1];
delete this._onItemCreateQueue[id];
break;
}
};
/**
* Handle the web worker response.
*
* @private
* @param {Object} data
* @return {undefined}
*/
ConnectionRouteManager3.prototype._handleWorkerResponse = function (data) {
var response = data.data;
if (response === 'READY') {
this._processQueue();
this.workerReady = true;
} else { // Plot points?
// response = [cmd, args, result]
// Render connections?
var id,
collection = '_autorouterBoxes';
switch (response[0]) {
case 'getPathPoints':
id = response[1][0]; // first arg from request
var points = response[2]; // result
this._renderConnection(id, points);
break;
case 'routePaths':
var paths = response[1];
for (var i = paths.length; i--;) {
this._renderConnection.apply(this, paths[i]);
}
break;
case 'addPath':
// Set the collection to store it then fall through
// to create the promise in the 'addBox' method
collection = '_autorouterPaths'; // jshint ignore:line
case 'addBox':
// Record the item
id = response[1][1];
this[collection][id] = response[2];
// Evaluate the event queue for the item in order
if (this._onItemCreateQueue[id]) { // FIXME: Sometimes this is null -> it is
for (i = 0; i < this._onItemCreateQueue[id].length; i++) {
this._onItemCreateQueue[id][i].call(this, response[2]);
}
}
this._onItemCreateQueue[id] = null;
break;
case 'BugReplayList':
if (DEBUG) {
this.download('AR_bug_report' + new Date().getTime() + '.json', response[1]);
}
this.readyToDownload = false;
break;
}
}
};
ConnectionRouteManager3.prototype.initialize = function () {
this._clearGraph();
//Adding event listeners
var self = this;
this._onComponentUpdate = function (_canvas, ID) {
if (self.diagramDesigner.itemIds.indexOf(ID) !== -1) {
if (self.diagramDesigner.items[ID].rotation !== self._autorouterBoxRotation[ID]) {
//Item has been rotated
var resizeFn = self._resizeItem.bind(self, ID);
self._modifyItem(ID, resizeFn);
}
} else if (self.diagramDesigner.connectionIds.indexOf(ID) !== -1) {
// If the path is being updated, refresh it
if (self._onItemCreateQueue[ID] !== undefined) { // Should have been added
self.deleteItem(ID);
self.insertConnection(ID);
} else {
self.logger.warn('Received update event for nonexistent path');
}
}
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ON_COMPONENT_UPDATE, this._onComponentUpdate);
this._onComponentCreate = function (_canvas, ID) {
if (self._onItemCreateQueue[ID] === undefined) { // New item
if (self.diagramDesigner.itemIds.indexOf(ID) !== -1) {
self.insertBox(ID);
} else if (self.diagramDesigner.connectionIds.indexOf(ID) !== -1) {
self.insertConnection(ID);
}
} else { // Already created the item
self.logger.warn('Received ON_COMPONENT_CREATE event for already created item! (' + ID + ')');
}
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ON_COMPONENT_CREATE, this._onComponentCreate);
this._onComponentResize = function (_canvas, ID) {
if (self._onItemCreateQueue[ID.ID] !== undefined) {
var resizeFn = self._resizeItem.bind(self, ID.ID);
self._modifyItem(ID.ID, resizeFn);
} else {
self.logger.warn('Received ITEM_SIZE_CHANGED event for nonexistent item! (' + ID.ID + ')');
self.insertBox(ID.ID);
}
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ITEM_SIZE_CHANGED, this._onComponentResize);
this._onComponentDelete = function (_canvas, ID) { // Boxes and lines
if (self._onItemCreateQueue[ID] !== undefined) {
self.deleteItem(ID);
} else {
self.logger.warn('Received ON_COMPONENT_DELETE event for nonexistent item! (' + ID + ')');
}
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ON_COMPONENT_DELETE, this._onComponentDelete);
//ON_UNREGISTER_SUBCOMPONENT
this._onItemPositionChanged = function (_canvas, eventArgs) {
if (self._onItemCreateQueue[eventArgs.ID] !== undefined) {
var x = self.diagramDesigner.items[eventArgs.ID].getBoundingBox().x,
y = self.diagramDesigner.items[eventArgs.ID].getBoundingBox().y;
self._modifyItem(eventArgs.ID, self._invokeAutoRouterMethod
.bind(self, 'move', [eventArgs.ID, {x: x, y: y}]));
} else {
self.logger.warn('Received ITEM_POSITION_CHANGED event for nonexistent item! (' + eventArgs.ID + ')');
}
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ITEM_POSITION_CHANGED,
this._onItemPositionChanged);
this._onClear = function () {
self._clearGraph();
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ON_CLEAR, this._onClear);
this._onUnregisterSubcomponent = function (sender, ids) {
var longid = ids.objectID + DESIGNERITEM_SUBCOMPONENT_SEPARATOR + ids.subComponentID;
if (self._onItemCreateQueue[longid] !== undefined) {
self.deleteItem(longid);
} else {
self.logger.warn('Received UNREGISTER event for nonexistent item! (' + longid + ')');
}
};
this.diagramDesigner.addEventListener(this.diagramDesigner.events.ON_UNREGISTER_SUBCOMPONENT,
this._onUnregisterSubcomponent);
};
ConnectionRouteManager3.prototype.destroy = function () {
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ON_COMPONENT_CREATE,
this._onComponentCreate);
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ON_COMPONENT_UPDATE,
this._onComponentUpdate);
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ITEM_SIZE_CHANGED,
this._onComponentResize);
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ON_COMPONENT_DELETE,
this._onComponentDelete);
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ITEM_POSITION_CHANGED,
this._onItemPositionChanged);
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ON_CLEAR, this._onClear);
this.diagramDesigner.removeEventListener(this.diagramDesigner.events.ON_UNREGISTER_SUBCOMPONENT,
this._onUnregisterSubcomponent);
if (this.worker) {
this.worker.terminate();
}
};
ConnectionRouteManager3.prototype._shouldRouteConnection = function (connId) {
var connInstance = this.diagramDesigner.items[connId];
if (!connInstance || connInstance.segmentPoints.length > 0 || connInstance.isBezier) {
// These should not be handled by the AutoRouter
// However it looks like it should support segmentPoints...)
return false;
}
return true;
};
ConnectionRouteManager3.prototype.redrawConnections = function (ids) {
var partialPreDraw = false;
if (ids.length !== this.diagramDesigner.connectionIds.length) {
partialPreDraw = true;
}
this.simpleRouter.redrawConnections(ids, partialPreDraw);
if (!this._initialized) {
this._initializeGraph();
}
this._invokeAutoRouterMethod('routeAsync', []);
};
/**
* Query the connection info from the autorouter and initiate a redraw
*
* @param {Array<String>} [ids] - Connection ids to redraw
* @return {Array<String>} ids - Updated Ids
*/
ConnectionRouteManager3.prototype.renderConnections = function (ids) {
var idList = ids || this.diagramDesigner.connectionIds.slice(0);
for (var i = idList.length; i--;) {
if (this._autorouterPaths[idList[i]]) {
this._invokeAutoRouterMethod('getPathPoints', [idList[i]]);
}
}
return idList;
};
/**
* Render the given connection in the WebGME
*
* @param {ConnectionId} id
* @param {Array<Points>} points
* @return {undefined}
*/
ConnectionRouteManager3.prototype._renderConnection = function (id, points) {
if (this.diagramDesigner.items[id]) { // Only render if the box still exists
this.diagramDesigner.items[id].setConnectionRenderData(points);
}
};
ConnectionRouteManager3.prototype._clearGraph = function () {
this._invokeAutoRouterMethod('clear', []);
this._autorouterBoxRotation = {}; // Define container that will map obj+subID -> rotation
this.endpointConnectionAreaInfo = {};
this.initialized = false;
this.readyToDownload = true;
this._onItemCreateQueue = {};
};
ConnectionRouteManager3.prototype._initializeGraph = function () {
/*
* In this method, we will update the boxes using the canvas.itemIds list and
* add any ports as needed (from the canvas.connectionIds)
*/
var canvas = this.diagramDesigner,
connIdList = canvas.connectionIds,
itemIdList = canvas.itemIds,
i = itemIdList.length;
while (i--) {
this.insertBox(itemIdList[i]);
}
i = connIdList.length;
while (i--) {
this.insertConnection(connIdList[i]);
}
this._initialized = true;
};
ConnectionRouteManager3.prototype._processQueue = function () {
for (var i = 0; i < this.workerQueue.length; i++) {
this.worker.postMessage(this.workerQueue[i]);
}
this.workerQueue = [];
};
ConnectionRouteManager3.prototype.insertConnection = function (connId) {
var canvas = this.diagramDesigner,
srcObjId,
srcSubCompId,
dstObjId,
dstSubCompId,
sId,
tId,
connMetaInfo,
srcConnAreas,
dstConnAreas,
srcPorts,
dstPorts,
j;
if (this._shouldRouteConnection(connId) === false) {
// These should not be handled by the AutoRouter
// (However it looks like it should support segmentPoints...
return;
}
srcObjId = canvas.connectionEndIDs[connId].srcObjId;
srcSubCompId = canvas.connectionEndIDs[connId].srcSubCompId;
dstObjId = canvas.connectionEndIDs[connId].dstObjId;
dstSubCompId = canvas.connectionEndIDs[connId].dstSubCompId;
sId = srcSubCompId ? srcObjId + DESIGNERITEM_SUBCOMPONENT_SEPARATOR + srcSubCompId : srcObjId;
tId = dstSubCompId ? dstObjId + DESIGNERITEM_SUBCOMPONENT_SEPARATOR + dstSubCompId : dstObjId;
connMetaInfo = canvas.items[connId].getMetaInfo();
srcConnAreas = canvas.items[srcObjId].getConnectionAreas(srcSubCompId, false, connMetaInfo);
dstConnAreas = canvas.items[dstObjId].getConnectionAreas(dstSubCompId, true, connMetaInfo);
srcPorts = {};
dstPorts = {};
this._updatePort(srcObjId, srcSubCompId);//Adding ports for connection
this._updatePort(dstObjId, dstSubCompId);
//Get available ports for this connection
j = srcConnAreas.length;
while (j--) {
srcPorts[srcConnAreas[j].id] = sId;
}
j = dstConnAreas.length;
while (j--) {
dstPorts[dstConnAreas[j].id] = tId;
}
// If it has both a src and dst
if (srcPorts.length !== 0 && dstPorts.length !== 0) {
this._invokeAutoRouterMethod('addPath',
[{src: srcPorts, dst: dstPorts}, connId]);
}
//Set custom points, if applicable
if (canvas.items[connId].segmentPoints.length > 0) {
var conn = canvas.items[connId],
customPoints = conn.segmentPoints.slice();
this._invokeAutoRouterMethod('setPathCustomPoints',
[{'path': connId, 'points': customPoints}]);
}
};
ConnectionRouteManager3.prototype.insertBox = function (objId) {
var canvas = this.diagramDesigner,
designerItem,
areas,
bBox,
boxdefinition,
isEnd,
j = 0;
designerItem = canvas.items[objId];
bBox = designerItem.getBoundingBox();
areas = designerItem.getConnectionAreas(objId, isEnd) || [];
boxdefinition = {
//BOX
'x1': bBox.x,
'y1': bBox.y,
'x2': bBox.x2,
'y2': bBox.y2,
//PORTS
'ports': []
};
while (j < areas.length) {
//Building up the ports object
boxdefinition.ports.push({
'id': areas[j].id, 'area': [[areas[j].x1, areas[j].y1], [areas[j].x2, areas[j].y2]],
'angles': [areas[j].angle1, areas[j].angle2]
});
j++;
}
this._invokeAutoRouterMethod('addBox', [boxdefinition, objId]);
this._autorouterBoxRotation[objId] = canvas.items[objId].rotation;
};
ConnectionRouteManager3.prototype.deleteItem = function (objId) {
// If I can query them from the objId, I can clear the entries with that info
// Make sure that the path/box has been created
var removeFn = this._invokeAutoRouterMethod.bind(this, 'remove', [objId]);
this._modifyItem(objId, removeFn);
};
ConnectionRouteManager3.prototype._resizeItem = function (objId) {
if (!this.diagramDesigner.items[objId]) {
//FIXME why is there message for nonexisting objects???
return;
}
var canvas = this.diagramDesigner,
isEnd = true,
connectionMetaInfo,
designerItem = canvas.items[objId],
newCoord = designerItem.getBoundingBox(),
newBox = {
x1: newCoord.x,
x2: newCoord.x2,
y1: newCoord.y,
y2: newCoord.y2
},
ports = [],
connAreas = designerItem.getConnectionAreas(objId, isEnd, connectionMetaInfo),
i;
// Create the new box connection areas
i = connAreas.length;
while (i--) {
// Building up the ConnectionAreas object
ports.push({
id: connAreas[i].id,
area: [[connAreas[i].x1, connAreas[i].y1], [connAreas[i].x2, connAreas[i].y2]],
angles: [connAreas[i].angle1, connAreas[i].angle2]
});
}
// Update Box
this._invokeAutoRouterMethod('setBoxRect', [objId, newBox]);
// Update box ports
for (i = ports.length; i--;) {
this._invokeAutoRouterMethod('updatePort', [objId, ports[i]]);
}
};
ConnectionRouteManager3.prototype._updatePort = function (objId, subCompId) {
var longid = objId + DESIGNERITEM_SUBCOMPONENT_SEPARATOR + subCompId,
newBox,
updateBoxFn;
if (subCompId !== undefined) { //Updating a port
// We need to know if the box even exists...
if (this._onItemCreateQueue[longid] === undefined) { // If the port doesn't exist, create it
this._createPort(objId, subCompId);
} else {
// TODO Adjust size, connection info
newBox = this._createPortInfo(objId, subCompId);
updateBoxFn = this._invokeAutoRouterMethod.bind(this, 'setBoxRect', [longid, newBox]);
this._modifyItem(longid, updateBoxFn);
}
} else { // Updating the box's connection areas
this._modifyItem(objId, this._updateBoxConnectionAreas.bind(this, objId));
}
};
/**
* Call a function on an item which may or may not exist yet
*
* @param {String} id
* @param {Function} fn
* @return {undefined}
*/
ConnectionRouteManager3.prototype._modifyItem = function (id, fn) {
if (this._onItemCreateQueue[id] === null) { // Item has been created
fn();
} else if (this._onItemCreateQueue[id] instanceof Array) { // Store the operation
this._onItemCreateQueue[id].push(fn);
} else {
// Avoids exception to be thrown here..
}
};
ConnectionRouteManager3.prototype._updateBoxConnectionAreas = function (objId) {
var areas = this.diagramDesigner.items[objId] ?
this.diagramDesigner.items[objId].getConnectionAreas() || [] : [],
newIds = {},
connInfo = [],
boxObject = this._autorouterBoxes[objId],
id,
j;
for (j = areas.length; j--;) {
//Building up the ports object
connInfo.push({
'id': areas[j].id, 'area': [[areas[j].x1, areas[j].y1], [areas[j].x2, areas[j].y2]],
'angles': [areas[j].angle1, areas[j].angle2]
});
newIds[areas[j].id] = true;
}
// Update each AutoRouter port
for (j = connInfo.length; j--;) {
this._invokeAutoRouterMethod('updatePort', [objId, connInfo[j]]);
}
if (!boxObject) {
//FIXME: It may or may not be safe to assume empty array..)
// Sentry will log the message if there is an uncaught exception after this..
this.logger.error('ConnectionRouteManager3: no boxObject, related to #1289');
return;
}
for (j = boxObject.ports.length; j--;) {
id = boxObject.ports[j].id;
if (!newIds[id]) {
// TODO: There is no API call in the AutoRouter/ActionApplier
// TODO: for removing port based on ids (this never worked)
//this._invokeAutoRouterMethod('removePort', [boxObject.ports[j]]); // Not sure FIXME
}
}
};
ConnectionRouteManager3.prototype._createPort = function (objId, subCompId) {
var longid = objId + DESIGNERITEM_SUBCOMPONENT_SEPARATOR + subCompId,
newBox = this._createPortInfo(objId, subCompId);
this._invokeAutoRouterMethod('addBox', [newBox, longid]);
this._invokeAutoRouterMethod('setComponent', [objId, longid]);
};
ConnectionRouteManager3.prototype._createPortInfo = function (objId, subCompId) {
//Ports will now be a subcomponent
//We will do the following:
// - Create a box for the port
// - Determine the connection areas
// - Determine the box
// - Use the connection angle
// - Set the box as a component of the parent
var canvas = this.diagramDesigner,
connectionMetaInfo = null,
areas = canvas.items[objId].getConnectionAreas(subCompId, true, connectionMetaInfo) || [],
j = areas.length,
newBox = {
'x1': null,
'x2': null,
'y1': null,
'y2': null,
'ports': []
};
while (j--) {
var angles = [areas[j].angle1, areas[j].angle2],
x1 = Math.min(areas[j].x1, areas[j].x2),
x2 = Math.max(areas[j].x1, areas[j].x2),
y1 = Math.min(areas[j].y1, areas[j].y2),
y2 = Math.max(areas[j].y1, areas[j].y2);
newBox.ports.push({
'id': areas[j].id, 'area': [[x1, y1], [x2, y2]],
'angles': angles
});
if (angles) {
var a1 = angles[0], //min angle
a2 = angles[1], //max angle
rightAngle = 0,
bottomAngle = 90,
leftAngle = 180,
topAngle = 270;
if (rightAngle < a1 || rightAngle > a2) {
x2 += 5;
}
if (leftAngle < a1 || leftAngle > a2) {
x1 -= 5;
}
if (topAngle < a1 || topAngle > a2) {
y1 -= 5;
}
if (bottomAngle < a1 || bottomAngle > a2) {
y2 += 5;
}
} else {
if (x2 - x1 < 3) {
x2 += 3;
}
if (y2 - y1 < 3) {
y2 += 3;
}
}
//Derive the box object
newBox.x2 = Math.max(x2 + 1, newBox.x2) || x2 + 1;
newBox.y1 = Math.min(y1 - 1, newBox.y1) || y1 - 1;
newBox.x1 = Math.min(x1 - 1, newBox.x1) || x1 - 1;
newBox.y2 = Math.max(y2 + 1, newBox.y2) || y2 + 1;
}
return newBox;
};
ConnectionRouteManager3.prototype.download = function (filename, data) {
var self = this;
if (!this.readyToDownload) {
return;
}
if (!filename) {
filename = 'console.json';
}
Saver.saveJsonToDisk(filename, data, self.logger, function (err, downloadUrl) {
if (err) {
self.logger.error('downloading resource for error handling failed', {metadata: {error: err}});
}
self.logger.error('Download url for auto-router debug data:', downloadUrl);
self.readyToDownload = false;
});
};
return ConnectionRouteManager3;
});