UNPKG

webgme

Version:

Web-based Generic Modeling Environment

708 lines (596 loc) 26.2 kB
/*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; });