diff --git a/SBOLCanvasBackend/src/utils/SBOLData.java b/SBOLCanvasBackend/src/utils/SBOLData.java index 09c5456a..77bca892 100644 --- a/SBOLCanvasBackend/src/utils/SBOLData.java +++ b/SBOLCanvasBackend/src/utils/SBOLData.java @@ -50,7 +50,8 @@ public class SBOLData { roles.put("RBS (Ribosome Binding Site)", SequenceOntology.RIBOSOME_ENTRY_SITE); roles.put("CDS (Coding Sequence)", SequenceOntology.CDS); roles.put("Ter (Terminator)", SequenceOntology.TERMINATOR); - roles.put("Cir (Circular Backbone)", SequenceOntology.CIRCULAR); + roles.put("Cir (Circular Backbone Left)", SequenceOntology.CIRCULAR); + roles.put("Cir (Circular Backbone Right)", SequenceOntology.CIRCULAR); roles.put("gRNA (Non-Coding RNA gene)", URI.create("http://identifiers.org/so/SO:0001263")); roles.put("Ori (Origin of Replication)", SequenceOntology.ORIGIN_OF_REPLICATION); roles.put("OriT (Origin of Transfer)", URI.create("http://identifiers.org/so/SO:0000724")); diff --git a/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.html b/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.html index 6cf5fb1a..a02e7cb6 100644 --- a/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.html +++ b/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.html @@ -6,8 +6,8 @@

Download

Server - + (selectionChange)="setRegistry($event.value)" value="{{registry}}"> + {{registry}} diff --git a/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.ts b/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.ts index 244233b4..ebc23545 100644 --- a/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.ts +++ b/SBOLCanvasFrontend/src/app/download-graph/download-graph.component.ts @@ -63,6 +63,16 @@ export class DownloadGraphComponent implements OnInit { constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialog: MatDialog, private metadataService: MetadataService, private graphService: GraphService, private filesService: FilesService, private loginService: LoginService, public dialogRef: MatDialogRef) { } ngOnInit() { + // check if there is a saved registry and collection information + if (this.metadataService.getSavedRegistry() !== undefined) this.registry = this.metadataService.getSavedRegistry(); + if (this.metadataService.getSavedCollection() !== undefined) { + this.collection = this.metadataService.getSavedCollection().collection; + this.history = this.metadataService.getSavedCollection().history; + } else { + this.collection = ""; + this.history = []; + } + this.working = true; if (this.data != null) { if (this.data.mode != null) { @@ -108,10 +118,9 @@ export class DownloadGraphComponent implements OnInit { this.working = false; }); } + this.updateParts(); this.parts.sort = this.sort; - this.history = []; - this.collection = ""; } loginDisabled(): boolean { @@ -128,6 +137,7 @@ export class DownloadGraphComponent implements OnInit { setRegistry(registry: string) { this.registry = registry; + this.metadataService.setSavedRegistry(registry); this.updateParts(); } @@ -172,7 +182,9 @@ export class DownloadGraphComponent implements OnInit { // only allowed to get here when there is one item selected, and it's a collection let row = this.selection.selected[0]; this.history.push(row); + this.metadataService.setSavedCollection({ collection: row.uri, history: this.history }); this.selection.clear(); + this.updateParts(); } @@ -222,6 +234,7 @@ export class DownloadGraphComponent implements OnInit { if (row.type === DownloadGraphComponent.collectionType) { this.history.push(row); this.collection = row.uri; + this.metadataService.setSavedCollection({ collection: this.collection, history: this.history }); this.selection.clear(); this.updateParts(); } else if (row.type === DownloadGraphComponent.componentType) { @@ -296,6 +309,7 @@ export class DownloadGraphComponent implements OnInit { changeCollection(collection: string) { this.selection.clear(); + let found = false; for (let i = 0; i < this.history.length; i++) { if (this.history[i] === collection) { @@ -306,7 +320,9 @@ export class DownloadGraphComponent implements OnInit { } if (!found) this.history.length = 0; + this.collection = collection; + this.metadataService.setSavedCollection({ collection: collection, history: this.history }); this.updateParts(); } @@ -356,7 +372,7 @@ export class DownloadGraphComponent implements OnInit { this.parts.data = partCache; this.working = false; }); - }else{ + }else { // collection, modules, and components this.partRequest = forkJoin( this.filesService.listParts(this.loginService.users[this.registry], this.registry, this.collection, null, null, "collections"), diff --git a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html index 19ab9e85..e01cc539 100644 --- a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html +++ b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html @@ -1,137 +1,152 @@
-
- - - - - - - - - - - - - - - - - - - - - Sequence Features - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + Sequence Features + + + + + + + + + + + + + + + - - - Molecular Species - - - - - - - - - - - - - - + + + + + + + + + - - - Interactions - - - - - - - - - - - - - - + + + + + + + + + - - - Interaction Nodes - - - - - - - - - - - - - - + + + + + + + + + - - - Util - - - - - - - - - - - - - - - - -
-
+ + + Util + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts index 4ebad558..f03d3706 100644 --- a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts +++ b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts @@ -149,6 +149,10 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { this.graphService.addBackbone(); } + addCircularPlasmid() { + this.graphService.addCircularPlasmid(); + } + addTextBox() { this.graphService.addTextBox(); } @@ -171,7 +175,4 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { keepOrder = (a, b) => { return a; } - /** - * Returns true if - */ -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/glyph.service.ts b/SBOLCanvasFrontend/src/app/glyph.service.ts index eb8682fc..f97f0cb5 100644 --- a/SBOLCanvasFrontend/src/app/glyph.service.ts +++ b/SBOLCanvasFrontend/src/app/glyph.service.ts @@ -46,7 +46,6 @@ export class GlyphService { 'assets/glyph_stencils/sequence_feature/ribonuclease-site.xml', 'assets/glyph_stencils/sequence_feature/rna-stability-element.xml', //'assets/glyph_stencils/sequence_feature/chromosomal-locus.xml', - //'assets/glyph_stencils/sequence_feature/circular-plasmid.xml', 'assets/glyph_stencils/sequence_feature/transcription-end.xml', 'assets/glyph_stencils/sequence_feature/translation-end.xml', //'assets/glyph_stencils/sequence_feature/test.xml', @@ -79,7 +78,7 @@ export class GlyphService { 'assets/glyph_stencils/interaction_nodes/dissociation.xml', 'assets/glyph_stencils/interaction_nodes/process.xml', 'assets/glyph_stencils/molecular_species/replacement-glyph.xml', - ] + ]; private indicatorXMLBundle: string = "assets/glyph_stencils/indicators/bundle.xml"; private indicatorXMLs: string[] = [ @@ -91,6 +90,8 @@ export class GlyphService { private utilXMLBundle: string = "assets/glyph_stencils/util/bundle.xml"; private utilXMLs: string[] = [ 'assets/backbone.xml', + 'assets/circular-plasmid-left.xml', + 'assets/circular-plasmid-right.xml', 'assets/textBox.xml', 'assets/module.xml', ]; @@ -105,7 +106,7 @@ export class GlyphService { private xmlBundle: string = "assets/glyph_stencils/bundle.xml" constructor() { - this.loadXMLBundle(this.xmlBundle) + this.loadXMLBundle(this.xmlBundle); } loadXMLBundle(bundleFile) { @@ -126,10 +127,12 @@ export class GlyphService { } } + // unused now loadXMLs(xml_list, glyph_list) { xml_list.forEach((filename) => this.loadXML(filename, glyph_list)); } + // unused now loadXML(xmlFile, glyph_list) { let req = mx.mxUtils.load(xmlFile); let root = req.getDocumentElement(); @@ -160,7 +163,7 @@ export class GlyphService { canvas.setStrokeColor('#000000'); canvas.setFillColor('none'); - + stencil.drawShape(canvas, shape, 0, 0, 50, 50); svgs[name] = elt; @@ -185,6 +188,10 @@ export class GlyphService { return this.interactionNodes; } + getUtilGlyphs() { + return this.utils; + } + getUtilElements() { return this.getElements(this.utils) } @@ -204,4 +211,4 @@ export class GlyphService { getSequenceFeatureElements() { return this.getElements(this.sequenceFeatures); } -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/glyphInfo.ts b/SBOLCanvasFrontend/src/app/glyphInfo.ts index 0a586c19..19608d45 100644 --- a/SBOLCanvasFrontend/src/app/glyphInfo.ts +++ b/SBOLCanvasFrontend/src/app/glyphInfo.ts @@ -22,37 +22,23 @@ export class GlyphInfo extends Info { derivedFroms: string[]; generatedBys: string[]; - constructor({ - id, - version = "1", - partType = "DNA region", - partRole, - }: { - id?, - version?, - partType?, - partRole?, - } = {}) { + constructor(partType?: string, id?: string) { super(); - this.version = version - this.partType = partType - this.partRole = partRole + this.version = "1" - // try to make a prefix from the part role - const partRolePrefix = partRole && (partRole.match(/(\w+?) \(/) || [])[1]; - - // generate id - this.displayID = id || partRolePrefix ? - `${partRolePrefix}_${customAlphabet(alphanumeric, 4)()}` : // either use prefix and short ID - customAlphabet(alphanumeric, 8)() // or long ID - - // ensure ID doesn't start with a digit - if(/^\d/.test(this.displayID)) - this.displayID = "i" + this.displayID + if (id) { + this.displayID = id + // this.displayID = 'id_' + (customAlphabet(alphanumeric, 5)()); + } + else + this.displayID = 'id' + (GlyphInfo.counter++); - // make a name so the displayed stuff is cleaner - this.name = partRolePrefix || " " + if (partType) { + this.partType = partType; + } else { + this.partType = 'DNA region'; + } } makeCopy() { @@ -152,4 +138,4 @@ export class GlyphInfo extends Info { return node; } -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/graph-base.ts b/SBOLCanvasFrontend/src/app/graph-base.ts index bfd23e64..f757a37e 100644 --- a/SBOLCanvasFrontend/src/app/graph-base.ts +++ b/SBOLCanvasFrontend/src/app/graph-base.ts @@ -291,18 +291,18 @@ export class GraphBase { reconstructCellStyle = true; else if (cell.style === GraphBase.STYLE_MOLECULAR_SPECIES || cell.style.includes(GraphBase.STYLE_MOLECULAR_SPECIES + ";")) reconstructCellStyle = true; - else if (cell.style === GraphBase.STYLE_INTERACTION || cell.style.includes(GraphBase.STYLE_INTERACTION+";")) + else if (cell.style === GraphBase.STYLE_INTERACTION || cell.style.includes(GraphBase.STYLE_INTERACTION + ";")) reconstructCellStyle = true; - else if (cell.style === GraphBase.STYLE_INTERACTION_NODE || cell.style.includes(GraphBase.STYLE_INTERACTION_NODE+";")) + else if (cell.style === GraphBase.STYLE_INTERACTION_NODE || cell.style.includes(GraphBase.STYLE_INTERACTION_NODE + ";")) reconstructCellStyle = true; } // reconstruct the cell style if (reconstructCellStyle) { if (glyphDict[cell.value] != null) { - if(glyphDict[cell.value] instanceof ModuleInfo){ + if (glyphDict[cell.value] instanceof ModuleInfo) { // module - if(!cell.style){ + if (!cell.style) { cell.style = GraphBase.STYLE_MODULE; } cell.geometry.width = GraphBase.defaultModuleWidth; @@ -318,7 +318,7 @@ export class GraphBase { cell.geometry.width = GraphBase.sequenceFeatureGlyphWidth; if (cell.geometry.height == 0) cell.geometry.height = GraphBase.sequenceFeatureGlyphHeight; - } else if(glyphDict[cell.value] instanceof GlyphInfo){ + } else if (glyphDict[cell.value] instanceof GlyphInfo) { // molecular species if (!cell.style) cell.style = GraphBase.STYLE_MOLECULAR_SPECIES + "macromolecule"; @@ -329,17 +329,17 @@ export class GraphBase { } } else if (interactionDict[cell.value] != null) { let intInfo = interactionDict[cell.value]; - if(cell.isVertex()){ + if (cell.isVertex()) { // interaction node let name = graphBaseRef.interactionNodeTypeToName(intInfo.interactionType); - if(!cell.style){ - cell.style = GraphBase.STYLE_INTERACTION_NODE+name; - }else{ + if (!cell.style) { + cell.style = GraphBase.STYLE_INTERACTION_NODE + name; + } else { cell.style = cell.style.replace(GraphBase.STYLE_INTERACTION_NODE, GraphBase.STYLE_INTERACTION_NODE + name); } cell.geometry.width = GraphBase.interactionNodeGlyphWidth; cell.geometry.height = GraphBase.interactionNodeGlyphHeight; - }else{ + } else { // interaction let name = intInfo.interactionType; if (name == "Biochemical Reaction" || name == "Non-Covalent Binding" || name == "Genetic Production") { @@ -528,8 +528,9 @@ export class GraphBase { const layout = new mx.mxStackLayout(graph, true); layout.resizeParent = true; layout.isVertexIgnored = function (vertex) { - return vertex.isBackbone() + return vertex.isBackbone(); }; + layout.execute(this); }; @@ -775,13 +776,14 @@ export class GraphBase { // we need this if we intend on creating custom shapes with stencils let sequenceFeatureStencils = this.glyphService.getSequenceFeatureGlyphs(); + let utilStencils = this.glyphService.getUtilGlyphs() mx.mxCellRenderer.prototype.createShape = function (state) { var shape = null; if (state.style != null) { let stencilName = state.style[mx.mxConstants.STYLE_SHAPE]; var stencil = mx.mxStencilRegistry.getStencil(stencilName); - if (sequenceFeatureStencils[stencilName] != null) { + if (sequenceFeatureStencils[stencilName] != null || utilStencils[stencilName] != null) { shape = new CustomShapes.SequenceFeatureShape(stencil); } else if (stencil != null) { shape = new mx.mxShape(stencil); @@ -794,47 +796,48 @@ export class GraphBase { return shape; } + const registerSequenceFeatureShapes = stencils => { + for (const name in stencils) { + // Create a new copy of the stencil for the graph. + const stencil = stencils[name][0]; + const centered = stencils[name][1]; + let customStencil = new mx.mxStencil(stencil.desc); // Makes a deep copy - // custom stencil setup - let stencils = this.glyphService.getSequenceFeatureGlyphs(); + // Change the copied stencil for mxgraph + let origDrawShape = mx.mxStencil.prototype.drawShape; - for (const name in stencils) { - // Create a new copy of the stencil for the graph. - const stencil = stencils[name][0]; - const centered = stencils[name][1]; - let customStencil = new mx.mxStencil(stencil.desc); // Makes a deep copy - - // Change the copied stencil for mxgraph - let origDrawShape = mx.mxStencil.prototype.drawShape; + if (centered) { + customStencil.drawShape = function (canvas, shape, x, y, w, h) { + h /= 2; + y += h / 2; + origDrawShape.apply(this, [canvas, shape, x, y, w, h]); - if (centered) { - customStencil.drawShape = function (canvas, shape, x, y, w, h) { - h /= 2; - y += h / 2; - origDrawShape.apply(this, [canvas, shape, x, y, w, h]); - - shape.paintComposite(canvas, x, y - (h / 2), w, h * 2); - } - } else { - customStencil.drawShape = function (canvas, shape, x, y, w, h) { - h = h / 2; - origDrawShape.apply(this, [canvas, shape, x, y, w, h]); + shape.paintComposite(canvas, x, y - (h / 2), w, h * 2); + } + } else { + customStencil.drawShape = function (canvas, shape, x, y, w, h) { + h = h / 2; + origDrawShape.apply(this, [canvas, shape, x, y, w, h]); - shape.paintComposite(canvas, x, y, w, h * 2); + shape.paintComposite(canvas, x, y, w, h * 2); + } } - } - // Add the stencil to the registry and set its style. - mx.mxStencilRegistry.addStencil(name, customStencil); + // Add the stencil to the registry and set its style. + mx.mxStencilRegistry.addStencil(name, customStencil); - const newGlyphStyle = mx.mxUtils.clone(this.baseSequenceFeatureGlyphStyle); - newGlyphStyle[mx.mxConstants.STYLE_SHAPE] = name; - this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_SEQUENCE_FEATURE + name, newGlyphStyle); + const newGlyphStyle = mx.mxUtils.clone(this.baseSequenceFeatureGlyphStyle); + newGlyphStyle[mx.mxConstants.STYLE_SHAPE] = name; + this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_SEQUENCE_FEATURE + name, newGlyphStyle); + } } + registerSequenceFeatureShapes(this.glyphService.getSequenceFeatureGlyphs()) + registerSequenceFeatureShapes(this.glyphService.getUtilGlyphs()) + // molecularSpecies glyphs are simpler, since we don't have to morph // them to always be centred on the strand - stencils = this.glyphService.getMolecularSpeciesGlyphs(); + let stencils = this.glyphService.getMolecularSpeciesGlyphs(); for (const name in stencils) { const stencil = stencils[name][0]; let customStencil = new mx.mxStencil(stencil.desc); // Makes of deep copy of the stencil. @@ -987,10 +990,10 @@ export class GraphBase { infoCopy.targetRefinement = {}; // add back refinements relating to ours - if(sourceRefinement){ + if (sourceRefinement) { infoCopy.sourceRefinement[edge.getId()] = sourceRefinement; } - if(targetRefinement){ + if (targetRefinement) { infoCopy.targetRefinement[edge.getId()] = targetRefinement; } @@ -1015,28 +1018,28 @@ export class GraphBase { let oldURI = edge.value; let nodeInfo = this.getFromInteractionDict(terminal.value).makeCopy(); this.graph.getModel().setValue(edge, nodeInfo.getFullURI()); - + // duplicate over the nescessary info // module targets - if(infoCopy.fromURI[edge.getId()]){ + if (infoCopy.fromURI[edge.getId()]) { nodeInfo.fromURI[edge.getId()] = infoCopy.fromURI[edge.getId()]; } - if(infoCopy.toURI[edge.getId()]){ + if (infoCopy.toURI[edge.getId()]) { nodeInfo.toURI[edge.getId()] = infoCopy.toURI[edge.getId()]; } // edge refinements let sourceRefinement = infoCopy.sourceRefinement[edge.getId()]; - if(sourceRefinement){ + if (sourceRefinement) { nodeInfo.sourceRefinement[edge.getId()] = sourceRefinement; } let targetRefinement = infoCopy.targetRefinement[edge.getId()]; - if(targetRefinement){ + if (targetRefinement) { nodeInfo.targetRefinement[edge.getId()] = targetRefinement; } // if the previous wasn't an interaction node, then we need to remove the info from the dictionary - if(!previous || !previous.isInteractionNode()){ + if (!previous || !previous.isInteractionNode()) { this.removeFromInteractionDict(oldURI); } @@ -1068,8 +1071,8 @@ export class GraphBase { // cell movement this.graph.addListener(mx.mxEvent.MOVE_CELLS, mx.mxUtils.bind(this, async function (sender, evt) { // sender is the graph - sender.getModel().beginUpdate(); + let cancelled = false; try { let movedCells = evt.getProperty("cells"); @@ -1077,7 +1080,7 @@ export class GraphBase { // can appear here (even if they were also selected) // sort cells: processing order is important - movedCells = movedCells.sort(function (cellA, cellB) { + movedCells = movedCells.sort(function (cellA, cellB) { if (cellA.getRootId() !== cellB.getRootId()) { // cells are not related: choose arbitrary order (but still group by root) return cellA.getRootId() < cellB.getRootId() ? -1 : 1; @@ -1125,6 +1128,7 @@ export class GraphBase { if (!movedCells[i].isSequenceFeatureGlyph()) { continue; } + // found a sequenceFeature glyph. A streak might be starting... const baseX = movedCells[i].getGeometry().x; const rootId = movedCells[i].getRootId(); @@ -1155,16 +1159,6 @@ export class GraphBase { this.horizontalSortBasedOnPosition(circuitContainer); } - // finallly, another special case: if a circuitContainer only has one sequenceFeatureGlyph, - // moving the glyph should move the circuitContainer - for (const cell of movedCells) { - if (cell.isSequenceFeatureGlyph() && cell.getParent().children.length === 2) { - const x = cell.getParent().getGeometry().x + evt.getProperty("dx"); - const y = cell.getParent().getGeometry().y + evt.getProperty("dy"); - cell.getParent().replaceGeometry(x, y, 'auto', 'auto', sender); - } - } - // sync circuit containers let circuitContainers = new Set(); for (let movedCell of movedCells) { @@ -1176,6 +1170,29 @@ export class GraphBase { this.syncCircuitContainer(circuitContainer); } + for (const cell of movedCells) { + // another special case: if a circuitContainer only has one sequenceFeatureGlyph, + // moving the glyph should move the circuitContainer + if (cell.isSequenceFeatureGlyph() && cell.getParent().children.length === 2) { + const x = cell.getParent().getGeometry().x + evt.getProperty("dx"); + const y = cell.getParent().getGeometry().y + evt.getProperty("dy"); + cell.getParent().replaceGeometry(x, y, "auto", "auto", sender); + } + + // special case where an empty circular backbone's circuit container is moved + // fixes the containers position and the right circular backbones x position + if((cell.circularBackbone && cell.children.length === 3)) { + this.repositionCircularBackbone(cell); + } + } + + // special case where a circular backbone is repositioned within a circuit container + if(movedCells[0].getParent().circularBackbone + && movedCells.filter(cell => cell.stayAtBeginning || cell.stayAtEnd).length > 0 + && movedCells[0].getParent().children.length === 3) { + this.repositionCircularBackbone(movedCells[0].getParent()); + } + // change ownership for (let container of Array.from(containers)) { this.changeOwnership(container); @@ -1210,7 +1227,7 @@ export class GraphBase { } let styleString = edge.style.slice(); - let startIdx = styleString.indexOf(GraphBase.STYLE_INTERACTION)+GraphBase.STYLE_INTERACTION.length; + let startIdx = styleString.indexOf(GraphBase.STYLE_INTERACTION) + GraphBase.STYLE_INTERACTION.length; let endIdx = styleString.indexOf(';', startIdx); endIdx = endIdx > 0 ? endIdx : styleString.length; let interactionType = styleString.slice(startIdx, endIdx); @@ -1224,7 +1241,7 @@ export class GraphBase { protected validateInteraction(interactionType: string, source: mxCell, target: mxCell) { // edges can't connect to edges - if((source && source.isEdge()) || (target && target.isEdge())){ + if ((source && source.isEdge()) || (target && target.isEdge())) { return "Edges are dissallowed to connect to edges."; } @@ -1323,4 +1340,4 @@ export class GraphBase { } } -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/graph-helpers.ts b/SBOLCanvasFrontend/src/app/graph-helpers.ts index af9ca3b7..338c63ec 100644 --- a/SBOLCanvasFrontend/src/app/graph-helpers.ts +++ b/SBOLCanvasFrontend/src/app/graph-helpers.ts @@ -830,12 +830,12 @@ export class GraphHelpers extends GraphBase { let interactionInfo = this.getFromInteractionDict(interaction.value).makeCopy(); // remove an edge if the new reference is null, and this specific edge had it's old reference if (!newReference) { - if(interactionInfo.fromURI[interaction.getId()] == oldReference){ + if (interactionInfo.fromURI[interaction.getId()] == oldReference) { delete interactionInfo.fromURI[interaction.getId()]; this.updateInteractionDict(interactionInfo); this.graph.getModel().remove(interaction); } - if(interactionInfo.toURI[interaction.getId()] == oldReference){ + if (interactionInfo.toURI[interaction.getId()] == oldReference) { delete interactionInfo.toURI[interaction.getId()]; this.updateInteractionDict(interactionInfo); this.graph.getModel().remove(interaction); @@ -843,13 +843,13 @@ export class GraphHelpers extends GraphBase { continue; } // replace any interaction references that reference the oldReference - for(let key in interactionInfo.fromURI){ - if(interactionInfo.fromURI[key] == oldReference){ + for (let key in interactionInfo.fromURI) { + if (interactionInfo.fromURI[key] == oldReference) { interactionInfo.fromURI[key] = newReference; } } - for(let key in interactionInfo.toURI){ - if(interactionInfo.toURI[key] == oldReference){ + for (let key in interactionInfo.toURI) { + if (interactionInfo.toURI[key] == oldReference) { interactionInfo.toURI[key] = newReference; } } @@ -1090,6 +1090,22 @@ export class GraphHelpers extends GraphBase { var cellsRemoved = evt.getProperty('added'); var cellsAdded = evt.getProperty('removed'); + // checks if either the left or right side of a circular backbone was selected + const cirBackboneFilter = sender.cells.filter(cell => cell.stayAtBeginning || cell.stayAtEnd); + + if(cirBackboneFilter.length > 0) { + const parentCell = cirBackboneFilter[0].parent.children; + const cirBackboneCells = [parentCell[parentCell.length - 1], parentCell[1]]; + + // checks if the circular backbone is already selected + if(this.graph.getSelectionCells()[0] == cirBackboneCells[0] && this.graph.getSelectionCells()[1] == cirBackboneCells[1]) { + return; + } + + //set the selection to the circular backbone cells + this.graph.setSelectionCells(cirBackboneCells); + } + console.debug("----handleSelectionChange-----"); console.debug("cells removed: "); @@ -1120,13 +1136,13 @@ export class GraphHelpers extends GraphBase { } /** - * Updates the data in the metadata service according to the cells properties - */ + * Updates the data in the metadata service according to the cells properties + */ protected updateAngularMetadata(cells) { // start with null data, (re)add it as possible this.nullifyMetadata(); - // if there is no current root it's because we're in the middle of reseting the view + // if there is no current root it's because we're in the middle of resetting the view if (!this.graph.getCurrentRoot()) return; @@ -1136,20 +1152,28 @@ export class GraphHelpers extends GraphBase { this.metadataService.setSelectedStyleInfo(styleInfo); } + // multiple selections can't display glyph data unless it's a circular backbone if (cells.length > 1) { - // multiple selections? can't display glyph data + if(cells[1].stayAtBeginning) { + let glyphInfo; + if (!cells[1]) glyphInfo = this.getFromInfoDict(this.graph.getCurrentRoot().getId()); + else glyphInfo = this.getFromInfoDict(cells[1].value); + + this.metadataService.setSelectedGlyphInfo(glyphInfo.makeCopy()); + } + return; } // have to add special check as no selection cell should signify the module/component of the current view let cell; - if (cells && cells.length > 0) { + if (cells && cells.length === 1) { cell = cells[0]; } if ((!cell && this.graph.getCurrentRoot().isModuleView()) || (cell && cell.isModule())) { let moduleInfo; - if (!cell) + if (!cell) moduleInfo = this.getFromInfoDict(this.graph.getCurrentRoot().getId()); else moduleInfo = this.getFromInfoDict(cell.value); @@ -1163,6 +1187,20 @@ export class GraphHelpers extends GraphBase { else glyphInfo = this.getFromInfoDict(cell.value); if (glyphInfo) { + if(cell.style === "circuitContainer") { + glyphInfo.sequence = ""; + + // appends the sequence of every child to the circuit containers sequence + cell.children.forEach(child => { + let childGlyphInfo; + if(child) childGlyphInfo = this.getFromInfoDict(child.value); + + if(childGlyphInfo !== undefined && childGlyphInfo.sequence) { + glyphInfo.sequence += childGlyphInfo.sequence; + } + }); + } + this.metadataService.setSelectedGlyphInfo(glyphInfo.makeCopy()); } } @@ -1631,10 +1669,10 @@ export class GraphHelpers extends GraphBase { // edge case, module view, need to check parent circuit container toCheck.add(cell.getParent().getValue()); } - } else if(cell.isCircuitContainer() && cell.getParent().isModuleView()){ + } else if (cell.isCircuitContainer() && cell.getParent().isModuleView()) { // transition state to module views toCheck.add(cell.getParent().getId()); - } else if(cell.isModule()){ + } else if (cell.isModule()) { toCheck.add(cell.getParent().getId()); } } @@ -1834,11 +1872,12 @@ export class GraphHelpers extends GraphBase { return false; } - protected flipInteractionEdge(cell){ - if(!cell.isInteraction()){ + protected flipInteractionEdge(cell) { + if (!cell.isInteraction()) { console.error("flipInteraction attempted on something other than an interaction!"); return; } + const src = cell.source; const dest = cell.target; this.graph.getModel().setTerminals(cell, dest, src); @@ -1847,10 +1886,10 @@ export class GraphHelpers extends GraphBase { let targetPoint = cell.geometry.getTerminalPoint(false); cell.geometry.setTerminalPoint(null, true); cell.geometry.setTerminalPoint(null, false); - if(sourcePoint){ + if (sourcePoint) { cell.geometry.setTerminalPoint(sourcePoint, false); } - if(targetPoint){ + if (targetPoint) { cell.geometry.setTerminalPoint(targetPoint, true); } // reverse the info to/from @@ -1859,10 +1898,10 @@ export class GraphHelpers extends GraphBase { let oldFrom = newInfo.toURI[cell.id]; delete newInfo.toURI[cell.id]; delete newInfo.fromURI[cell.id]; - if(oldTo){ + if (oldTo) { newInfo.fromURI[cell.id] = oldTo; } - if(oldFrom){ + if (oldFrom) { newInfo.toURI[cell.id] = oldFrom; } // nuke the refinemnets, as source refinements don't match target refinements @@ -2035,18 +2074,51 @@ export class GraphHelpers extends GraphBase { } } + /** + * If a circuit container contains only the container's width is for some + * reason set to 1 and the right side of the circular backbone is moved right next to the left + * side, this method fixes the formatting + * + * @param circuitContainer The circuit container that contains the circular backbone + */ + repositionCircularBackbone(circuitContainer) { + const childrenCopy = circuitContainer.children.slice().filter(cell => cell.stayAtEnd); + const containerCopy = childrenCopy[0].getParent(); + + containerCopy.replaceGeometry("auto", "auto", 52, "auto", this.graph); + childrenCopy[0].replaceGeometry( + childrenCopy[0].getGeometry().x + 49, "auto", "auto", "auto", this.graph); + } + horizontalSortBasedOnPosition(circuitContainer) { + // pull out children that should be sorted + let childrenCopy = circuitContainer.children.slice() + .filter(cell => !cell.stayAtBeginning); + // sort the children - let childrenCopy = circuitContainer.children.slice(); - childrenCopy.sort(function (cellA, cellB) { - return cellA.getGeometry().x - cellB.getGeometry().x; - }); + childrenCopy.sort((cellA, cellB) => + cellA.getGeometry().x - cellB.getGeometry().x + ); + // and have the model reflect the sort in an undoable way - for (let i = 0; i < childrenCopy.length; i++) { - const child = childrenCopy[i]; + childrenCopy.forEach((child, i) => { this.graph.getModel().add(circuitContainer, child, i); - } + }); + // add in children that should stay at the beginning + circuitContainer.children + .filter(cell => cell.stayAtBeginning) + .forEach(child => { + this.graph.getModel().add(circuitContainer, child, 0); + }); + + // add in children that should stay at the end + circuitContainer.children + .filter(cell => cell.stayAtEnd) + .forEach(child => { + this.graph.getModel().add(circuitContainer, child, circuitContainer.children.length); + }); + circuitContainer.refreshCircuitContainer(this.graph); } @@ -2073,4 +2145,4 @@ export class GraphHelpers extends GraphBase { protected showError(message: string) { this.dialog.open(ErrorComponent, { data: message }); } -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/graph.service.ts b/SBOLCanvasFrontend/src/app/graph.service.ts index 737529bb..bb91f6e9 100644 --- a/SBOLCanvasFrontend/src/app/graph.service.ts +++ b/SBOLCanvasFrontend/src/app/graph.service.ts @@ -40,7 +40,7 @@ export class GraphService extends GraphHelpers { // handle double click on glyph to enter it this.graph.addListener(mx.mxEvent.DOUBLE_CLICK, mx.mxUtils.bind(this, this.enterGlyph)); - + // --- For when SBOLCanvas is embedded in another app --- // send changes in mxgraph model to parent @@ -256,6 +256,12 @@ export class GraphService extends GraphHelpers { async flipSequenceFeatureGlyph() { let selectionCells = this.graph.getSelectionCells(); + // a circular backbone cannot be flipped + if(selectionCells.filter(cell => cell.stayAtBeginning || cell.stayAtEnd).length > 0) { + this.showError("A circular backbone cannot be flipped."); + return; + } + // flip any selected glyphs let parentInfos = new Set(); for (let cell of selectionCells) { @@ -290,6 +296,13 @@ export class GraphService extends GraphHelpers { this.graph.setCellStyles(mx.mxConstants.STYLE_DIRECTION, "east", [cell]); console.debug("turning east"); } + + // if a glyph has been flipped its sequence needs to be reversed + let glyphInfo; + if(cell) glyphInfo = this.getFromInfoDict(cell.value); + glyphInfo.sequence = glyphInfo.sequence.split("").reverse().join(""); + + this.metadataService.setSelectedGlyphInfo(glyphInfo); } else if (cell.isInteraction()) { this.flipInteractionEdge(cell); } else if (cell.isInteractionNode()) { @@ -301,7 +314,6 @@ export class GraphService extends GraphHelpers { } // sync circuit containers - let circuitContainers = []; for (let cell of selectionCells) { if (cell.isSequenceFeatureGlyph()) { this.syncCircuitContainer(cell.getParent()); @@ -448,14 +460,16 @@ export class GraphService extends GraphHelpers { let circuitContainers = []; for (let cell of selectedCells) { if (cell.isSequenceFeatureGlyph()) { - circuitContainers.push(cell.getParent()); - // if it's a sequence feature and it has a combinatorial, remove the variable component if (cell.isSequenceFeatureGlyph()) { let combinatorial = this.getCombinatorialWithTemplate(cell.getParent().getValue()); // TODO make this undoable if (combinatorial) combinatorial.removeVariableComponentInfo(cell.getId()); + + if(cell.stayAtBeginning || cell.stayAtEnd) cell.getParent().circularBackbone = false; + + circuitContainers.push(cell.getParent()); } } else if (cell.isCircuitContainer() && this.graph.getCurrentRoot() && this.graph.getCurrentRoot().isComponentView()) circuitContainers.push(cell); @@ -492,8 +506,6 @@ export class GraphService extends GraphHelpers { this.graph.setSelectionCells(newSelection); } - - // remove interactions with modules if the item it connects to is being removed for (let selectedCell of selectedCells) { if (selectedCell.isCircuitContainer() || selectedCell.isMolecularSpeciesGlyph()) { @@ -520,6 +532,12 @@ export class GraphService extends GraphHelpers { for (let cell of circuitContainers) { cell.refreshCircuitContainer(this.graph); } + + // repositions the circular backbone if the circular backbone is now empty + if(circuitContainers.length > 0 && circuitContainers[0].children.length === 3 + && circuitContainers[0].circularBackbone) { + this.repositionCircularBackbone(circuitContainers[0]); + } } finally { this.graph.getModel().endUpdate(); } @@ -608,8 +626,6 @@ export class GraphService extends GraphHelpers { this.graph.center(); } - - /** * Turns the given element into a dragsource for creating * sequenceFeatureGlyphs of the type specified by 'stylename.' @@ -625,7 +641,7 @@ export class GraphService extends GraphHelpers { * Adds a sequenceFeatureGlyph. * The new glyph's location is based off the user's selection. */ - addSequenceFeature(name) { + async addSequenceFeature(name) { this.graph.getModel().beginUpdate(); try { if (!this.atLeastOneCircuitContainerInGraph()) { @@ -658,7 +674,66 @@ export class GraphService extends GraphHelpers { } // Add it - this.addSequenceFeatureAt(name, x, y, circuitContainer); + await this.addSequenceFeatureAt(name, x, y, circuitContainer); + } finally { + this.graph.getModel().endUpdate(); + } + } + + async addCircularPlasmid() { + this.graph.getModel().beginUpdate(); + try { + if (!this.atLeastOneCircuitContainerInGraph()) { + // if there is no strand, quietly make one + // stupid user + this.addBackbone(); + // this changes the selection, so the rest of this method works fine + } + + // let the graph choose an arbitrary cell from the selection, + // we'll pretend it's the only one selected + const selection = this.graph.getSelectionCell(); + + // if selection is nonexistent, or is not part of a strand, there is no suitable place. + if (!selection || !(selection.isSequenceFeatureGlyph() || selection.isCircuitContainer())) { + return; + } + + const circuitContainer = selection.isCircuitContainer() ? selection : selection.getParent(); + + // there cannot be more than one circular backbone on a circuit container + if(circuitContainer.circularBackbone) return; + + circuitContainer.circularBackbone = true; + + // x is at the beginning of the circuit container + let x = circuitContainer.getGeometry().x; + + // use y coord of the strand + let y = circuitContainer.getGeometry().y; + + // add the left side of the circular cell + const circCellLeft = await this.addSequenceFeatureAt("Cir (Circular Backbone Left)", + x, y, circuitContainer, { + connectable: false, + glyphWidth: 1, + }); + circCellLeft.stayAtBeginning = true; + + // add the right side of the circular cell + const circCellRight = await this.addSequenceFeatureAt("Cir (Circular Backbone Right)", + x + circuitContainer.getGeometry().width, y, + circuitContainer, { + connectable: false, + glyphWidth: 1, + }); + circCellRight.stayAtEnd = true; + + // if the only cells are the backbone and the circular backbone the right circular backbone needs + // to be repositioned and the size of the circuit container needs to reflect that + if(circuitContainer.getGeometry().width == 2) { + this.repositionCircularBackbone(circuitContainer); + } } finally { this.graph.getModel().endUpdate(); } @@ -673,7 +748,14 @@ export class GraphService extends GraphHelpers { * x,y are also used to determine where on the strand the new * glyph is added (first, second, etc) */ - async addSequenceFeatureAt(name, x, y, circuitContainer?) { + async addSequenceFeatureAt(name, x, y, circuitContainer?, { + connectable = true, + glyphWidth = GraphBase.sequenceFeatureGlyphWidth, + glyphStyle = undefined, + cellValue = undefined, + } = {}) { + let sequenceFeatureCell; + let cirBackboneLeftCell; // ownership change check if (this.graph.getCurrentRoot()) { @@ -713,24 +795,45 @@ export class GraphService extends GraphHelpers { x = x - circuitContainer.getGeometry().x; y = y - circuitContainer.getGeometry().y; - // create the glyph info and add it to the dictionary - const glyphInfo = new GlyphInfo({ - partRole: name - }); - this.addToInfoDict(glyphInfo); + let glyphInfo = new GlyphInfo(); + + // if the container is a circular backbone then both sides should have the same cellValue + if (glyphWidth == 1) { + circuitContainer.children + .filter(cell => cell.stayAtBeginning) + .forEach(child => { + cellValue = child.value; + cirBackboneLeftCell = child; + }); + } + if(cellValue == null) { + // create the glyph info and add it to the dictionary + glyphInfo.partRole = name; + this.addToInfoDict(glyphInfo); + } + // Insert new glyph and its components - const sequenceFeatureCell = this.graph.insertVertex(circuitContainer, null, glyphInfo.getFullURI(), x, y, GraphBase.sequenceFeatureGlyphWidth, GraphBase.sequenceFeatureGlyphHeight, GraphBase.STYLE_SEQUENCE_FEATURE + name); + sequenceFeatureCell = this.graph.insertVertex( + circuitContainer, + null, + cellValue == null ? glyphInfo.getFullURI() : cellValue, + x, y, glyphWidth, GraphBase.sequenceFeatureGlyphHeight, + glyphStyle || GraphBase.STYLE_SEQUENCE_FEATURE + name + ); this.createViewCell(glyphInfo.getFullURI()); - sequenceFeatureCell.setConnectable(true); + sequenceFeatureCell.setConnectable(connectable); // Sorts the new SequenceFeature into the correct position in parent's array this.horizontalSortBasedOnPosition(circuitContainer); // The new glyph should be selected this.graph.clearSelection(); - this.graph.setSelectionCell(sequenceFeatureCell); + + // if the new sequence feature is a circular backbone both circular backbones should be selected + if(cirBackboneLeftCell !== undefined) this.graph.setSelectionCells([cirBackboneLeftCell, sequenceFeatureCell]); + else if(glyphWidth !== 1) this.graph.setSelectionCell(sequenceFeatureCell); // perform the ownership change if (this.graph.getCurrentRoot()) { @@ -752,6 +855,8 @@ export class GraphService extends GraphHelpers { } finally { this.graph.getModel().endUpdate(); } + + return sequenceFeatureCell; } /** @@ -780,9 +885,8 @@ export class GraphService extends GraphHelpers { try { //TODO partRoles for proteins - let proteinInfo = new GlyphInfo({ - partType: this.moleculeNameToType(name) - }); + let proteinInfo = new GlyphInfo(); + proteinInfo.partType = this.moleculeNameToType(name); this.addToInfoDict(proteinInfo); const molecularSpeciesGlyph = this.graph.insertVertex(this.graph.getDefaultParent(), null, proteinInfo.getFullURI(), x, y, @@ -795,6 +899,8 @@ export class GraphService extends GraphHelpers { } finally { this.graph.getModel().endUpdate(); } + + console.log(this.graph.getModel().cells); } makeInteractionNodeDragsource(element, stylename) { @@ -1038,7 +1144,7 @@ export class GraphService extends GraphHelpers { this.graph.getModel().endUpdate(); } } - +x /** * Find the selected cell, and if there is a glyph selected, update its metadata. */ @@ -1275,7 +1381,7 @@ export class GraphService extends GraphHelpers { /** * Decodes the given string (xml) representation of a cell - * and uses it ot replace the currently selected cell + * and uses it to replace the currently selected cell * @param cellString */ async setSelectedToXML(cellString: string) { @@ -1667,4 +1773,4 @@ export class GraphService extends GraphHelpers { return this.graph.getCurrentRoot() } -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts b/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts index 7d1d0fc9..feb95684 100644 --- a/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts +++ b/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts @@ -89,6 +89,8 @@ export class InfoEditorComponent implements OnInit { break; } case 'partRole': { + if(event.value.includes("Cir (Circular Backbone")) break; + this.glyphInfo.partRole = event.value; this.glyphInfo.partRefine = ''; if (event.value !== '') { diff --git a/SBOLCanvasFrontend/src/app/metadata.service.ts b/SBOLCanvasFrontend/src/app/metadata.service.ts index 6c4f9c73..1221f517 100644 --- a/SBOLCanvasFrontend/src/app/metadata.service.ts +++ b/SBOLCanvasFrontend/src/app/metadata.service.ts @@ -66,10 +66,32 @@ export class MetadataService { private componentDefinitionModeSource = new BehaviorSubject(null); componentDefinitionMode = this.componentDefinitionModeSource.asObservable(); + private savedRegistry: string; + private savedCollection: { collection: string, history: Array }; + // TODO: DNA strand info constructor(private http: HttpClient) { } + getSavedRegistry() { + return this.savedRegistry; + } + + getSavedCollection() { + return this.savedCollection; + } + + setSavedRegistry(registry: string) { + this.savedRegistry = registry; + } + + setSavedCollection(collectionInfo: { collection: string, history: Array }) { + this.savedCollection = { + collection: collectionInfo.collection, + history: collectionInfo.history + } + } + loadTypes(): Observable { return this.http.get(this.typesURL); } @@ -121,4 +143,4 @@ export class MetadataService { setComponentDefinitionMode(newSetting: boolean) { this.componentDefinitionModeSource.next(newSetting) } -} +} \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/sequenceFeatures/.bundle_order b/SBOLCanvasFrontend/src/assets/glyph_stencils/sequenceFeatures/.bundle_order index 6d883932..9b7f7aa2 100644 --- a/SBOLCanvasFrontend/src/assets/glyph_stencils/sequenceFeatures/.bundle_order +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/sequenceFeatures/.bundle_order @@ -33,5 +33,4 @@ translation-end !test !chromosomal-locus -!circular-plasmid !dna-stability-element \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/.bundle_order b/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/.bundle_order index 43729c32..72634336 100644 --- a/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/.bundle_order +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/.bundle_order @@ -1,3 +1,5 @@ backbone +circular-plasmid-left +circular-plasmid-right textBox module \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/circular-plasmid-left.xml b/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/circular-plasmid-left.xml new file mode 100644 index 00000000..9802d91f --- /dev/null +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/circular-plasmid-left.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/circular-plasmid-right.xml b/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/circular-plasmid-right.xml new file mode 100644 index 00000000..59cfb579 --- /dev/null +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/utils/circular-plasmid-right.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file