+
+
+ 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