diff --git a/SBOLCanvasBackend/src/data/InteractionInfo.java b/SBOLCanvasBackend/src/data/InteractionInfo.java index fa939d58..79c2d272 100644 --- a/SBOLCanvasBackend/src/data/InteractionInfo.java +++ b/SBOLCanvasBackend/src/data/InteractionInfo.java @@ -1,6 +1,6 @@ package data; -import utils.Converter; +import java.util.Hashtable; public class InteractionInfo extends Info { @@ -8,25 +8,18 @@ public class InteractionInfo extends Info { private String interactionType; private String fromParticipationType; private String toParticipationType; - private String fromURI; - private String toURI; - - public String getFromURI() { - return fromURI; - } - - public void setFromURI(String fromURI) { - this.fromURI = fromURI; - } - - public String getToURI() { - return toURI; - } + private Hashtable sourceRefinement; + private Hashtable targetRefinement; + private Hashtable fromURI; + private Hashtable toURI; - public void setToURI(String toURI) { - this.toURI = toURI; + public InteractionInfo() { + sourceRefinement = new Hashtable(); + targetRefinement = new Hashtable(); + fromURI = new Hashtable(); + toURI = new Hashtable(); } - + public String getDisplayID() { return displayID; } @@ -59,7 +52,39 @@ public void setToParticipationType(String toParticipationType) { this.toParticipationType = toParticipationType; } + public Hashtable getSourceRefinement() { + return sourceRefinement; + } + + public void setSourceRefinement(Hashtable sourceRefinement) { + this.sourceRefinement = sourceRefinement; + } + + public Hashtable getTargetRefinement() { + return targetRefinement; + } + + public void setTargetRefinement(Hashtable targetRefinement) { + this.targetRefinement = targetRefinement; + } + + public Hashtable getFromURI() { + return fromURI; + } + + public void setFromURI(Hashtable fromURI) { + this.fromURI = fromURI; + } + + public Hashtable getToURI() { + return toURI; + } + + public void setToURI(Hashtable toURI) { + this.toURI = toURI; + } + public String getFullURI() { - return Converter.URI_PREFIX+'/'+this.displayID; + return this.uriPrefix + '/' + this.displayID; } } diff --git a/SBOLCanvasBackend/src/servlets/Data.java b/SBOLCanvasBackend/src/servlets/Data.java index 4b0407d9..9d831140 100644 --- a/SBOLCanvasBackend/src/servlets/Data.java +++ b/SBOLCanvasBackend/src/servlets/Data.java @@ -39,6 +39,15 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t body = gson.toJson(SBOLData.getRefinement(parent)); } else if (request.getPathInfo().equals("/interactions")) { body = gson.toJson(SBOLData.getInteractions()); + }else if (request.getPathInfo().equals("/interactionRoles")) { + body = gson.toJson(SBOLData.getInteractionRoles()); + }else if (request.getPathInfo().equals("/interactionRoleRefine")) { + String parent = request.getParameter("parent"); + if(parent == null) { + response.setStatus(HttpStatus.SC_BAD_REQUEST); + return; + } + body = gson.toJson(SBOLData.getInteractionRoleRefinement(parent)); } // write it to the response body diff --git a/SBOLCanvasBackend/src/utils/Converter.java b/SBOLCanvasBackend/src/utils/Converter.java index 94388348..93746d4a 100644 --- a/SBOLCanvasBackend/src/utils/Converter.java +++ b/SBOLCanvasBackend/src/utils/Converter.java @@ -14,6 +14,7 @@ import data.CombinatorialInfo; import data.Info; +import data.InteractionInfo; public class Converter { @@ -24,6 +25,7 @@ public class Converter { // data constants public static final int INFO_DICT_INDEX = 0; public static final int COMBINATORIAL_DICT_INDEX = 1; + public static final int INTERACTION_DICT_INDEX = 2; // style constants protected static final String STYLE_CIRCUIT_CONTAINER = "circuitContainer"; @@ -37,6 +39,7 @@ public class Converter { protected static final String STYLE_INTERACTION = "interactionGlyph"; protected static final String STYLE_MODULE_VIEW = "moduleViewCell"; protected static final String STYLE_COMPONENT_VIEW = "componentViewCell"; + protected static final String STYLE_INTERACTION_NODE = "interactionNodeGlyph"; static { // Necessary for encoding/decoding GlyphInfo and InteractionInfo @@ -45,6 +48,7 @@ public class Converter { protected Hashtable infoDict; protected Hashtable combinatorialDict; + protected Hashtable interactionDict; protected LayoutHelper layoutHelper; /** @@ -107,6 +111,13 @@ public boolean filter(Object arg0) { } }; + static Filter interactionNodeFilter = new Filter() { + @Override + public boolean filter(Object arg0) { + return arg0 instanceof mxCell && ((mxCell) arg0).getStyle().contains(STYLE_INTERACTION_NODE); + } + }; + protected static URI getParticipantType(boolean source, Set interactionTypes) { if (interactionTypes.contains(SystemsBiologyOntology.BIOCHEMICAL_REACTION)) { return source ? SystemsBiologyOntology.REACTANT : SystemsBiologyOntology.PRODUCT; diff --git a/SBOLCanvasBackend/src/utils/MxToSBOL.java b/SBOLCanvasBackend/src/utils/MxToSBOL.java index 56ec9691..cb3d7ecd 100644 --- a/SBOLCanvasBackend/src/utils/MxToSBOL.java +++ b/SBOLCanvasBackend/src/utils/MxToSBOL.java @@ -31,6 +31,7 @@ import org.sbolstandard.core2.OperatorType; import org.sbolstandard.core2.Module; import org.sbolstandard.core2.OrientationType; +import org.sbolstandard.core2.Participation; import org.sbolstandard.core2.RefinementType; import org.sbolstandard.core2.RestrictionType; import org.sbolstandard.core2.SBOLConversionException; @@ -75,6 +76,7 @@ public MxToSBOL() { public MxToSBOL(HashMap userTokens) { infoDict = new Hashtable(); combinatorialDict = new Hashtable(); + interactionDict = new Hashtable(); this.userTokens = userTokens; } @@ -133,6 +135,7 @@ public SBOLDocument setupDocument(InputStream graphStream) throws IOException, U }else { combinatorialDict = (Hashtable) dataContainer.get(COMBINATORIAL_DICT_INDEX); } + interactionDict = (Hashtable) dataContainer.get(INTERACTION_DICT_INDEX); // cells may show up in the child array not based on their x location enforceChildOrdering(model, graph); @@ -148,12 +151,6 @@ public SBOLDocument setupDocument(InputStream graphStream) throws IOException, U SynBioHubFrontend registry = document.addRegistry(key); registry.setUser(userTokens.get(key)); } - // TODO come back when fetching collections from multiple registries works -// for(String registry : SBOLData.registries) { -// document.addRegistry(registry); -// if(userTokens.containsKey(registry)) -// document.getRegistry(registry).setUser(userTokens.get(registry)); -// } layoutHelper = new LayoutHelper(document, graph); @@ -440,10 +437,23 @@ private void createCombinatorial(SBOLDocument document, mxGraph graph, mxGraphMo private void linkModuleDefinition(SBOLDocument document, mxGraph graph, mxGraphModel model, mxCell viewCell) throws SBOLValidationException, TransformerFactoryConfigurationError, TransformerException, URISyntaxException { - mxCell[] edges = Arrays.stream(mxGraphModel.getChildCells(model, viewCell, false, true)).toArray(mxCell[]::new); mxCell[] viewChildren = Arrays.stream(mxGraphModel.getChildCells(model, viewCell, true, false)) .toArray(mxCell[]::new); mxCell[] modules = Arrays.stream(mxGraphModel.filterCells(viewChildren, moduleFilter)).toArray(mxCell[]::new); + mxCell[] interactionNodes = Arrays.stream(mxGraphModel.filterCells(viewChildren, interactionNodeFilter)).toArray(mxCell[]::new); + + // filter out all edges that connect to interaction nodes + ArrayList uniqueInteractionCells = new ArrayList(); + for(mxCell edge : Arrays.stream(mxGraphModel.getChildCells(model, viewCell, false, true)).toArray(mxCell[]::new)) { + if(((mxCell) edge.getSource()).getStyle().contains(STYLE_INTERACTION_NODE) || ((mxCell) edge.getTarget()).getStyle().contains(STYLE_INTERACTION_NODE)) { + continue; + } + uniqueInteractionCells.add(edge); + } + for(mxCell interactionNode : interactionNodes) { + uniqueInteractionCells.add(interactionNode); + } + mxCell[] interactionCells = uniqueInteractionCells.toArray(new mxCell[0]); ModuleDefinition modDef = document.getModuleDefinition(URI.create((String) viewCell.getId())); @@ -472,78 +482,48 @@ private void linkModuleDefinition(SBOLDocument document, mxGraph graph, mxGraphM } // edges to interactions - for (mxCell edge : edges) { - - // interaction - InteractionInfo intInfo = (InteractionInfo) edge.getValue(); + for(mxCell interactionCell : interactionCells) { + InteractionInfo intInfo = interactionDict.get(interactionCell.getValue()); Interaction interaction = null; - if (!layoutOnly) { - interaction = modDef.createInteraction(intInfo.getDisplayID(), - SBOLData.interactions.getValue(intInfo.getInteractionType())); - } else { + if(!layoutOnly) { + interaction = modDef.createInteraction(intInfo.getDisplayID(), SBOLData.interactions.getValue(intInfo.getInteractionType())); + }else { Set interactions = modDef.getInteractions(); // find the interaction with the correct identity - for (Interaction inter : interactions) { - if (inter.getIdentity().toString().equals((String) intInfo.getFullURI())) { + for(Interaction inter : interactions) { + if(inter.getIdentity().toString().equals((String) intInfo.getFullURI())){ interaction = inter; + break; } } } - layoutHelper.addGraphicalNode(modDef.getIdentity(), interaction.getDisplayId(), edge); - - // nothing below here is layout - if (layoutOnly) { + layoutHelper.addGraphicalNode(modDef.getIdentity(), interaction.getDisplayId(), interactionCell); + if(interactionCell.getStyle().contains(STYLE_INTERACTION_NODE) && layoutOnly) { + mxCell[] interactionEdges = Arrays.stream(mxGraphModel.getEdges(model, interactionCell)).toArray(mxCell[]::new); + for(mxCell interactionEdge : interactionEdges) { + attachEdgeToParticipant(document, model, modDef, interaction, layoutOnly, intInfo, interactionEdge); + } + } + + // No layout stuff below this part + if(layoutOnly) { return; } - - // participants - mxCell source = (mxCell) edge.getSource(); - mxCell sourceParent = null; - mxCell target = (mxCell) edge.getTarget(); - mxCell targetParent = null; - GlyphInfo sourceInfo = null; - GlyphInfo targetInfo = null; - if (source != null && source.getStyle().contains(STYLE_MODULE)) { - String fromCellID = intInfo.getFromURI().substring(intInfo.getFromURI().lastIndexOf("_") + 1); - String fromID = intInfo.getFromURI().substring(0, intInfo.getFromURI().lastIndexOf("_")); - sourceInfo = (GlyphInfo) infoDict.get(fromID); - sourceParent = source; - // get the actual source part (child of the module) - source = (mxCell) model.getCell(fromCellID); - } else if (source != null) { - sourceInfo = (GlyphInfo) infoDict.get(source.getValue()); - sourceParent = (mxCell) source.getParent(); - } - if (target != null && target.getStyle().contains(STYLE_MODULE)) { - String toCellID = intInfo.getToURI().substring(intInfo.getToURI().lastIndexOf("_") + 1); - String toID = intInfo.getToURI().substring(0, intInfo.getToURI().lastIndexOf("_")); - targetInfo = (GlyphInfo) infoDict.get(toID); - targetParent = target; - // get the actual target part (child of the module) - target = (mxCell) model.getCell(toCellID); - } else if (target != null) { - targetInfo = (GlyphInfo) infoDict.get(target.getValue()); - targetParent = (mxCell) target.getParent(); - } - - // source participant - if (source != null) { - FunctionalComponent sourceFC = getOrCreateParticipant(document, modDef, sourceInfo, source, - sourceParent); - interaction.createParticipation(sourceInfo.getDisplayID() + "_" + source.getId(), - sourceFC.getIdentity(), getParticipantType(true, interaction.getTypes())); - } - - // target participant - if (target != null) { - FunctionalComponent targetFC = getOrCreateParticipant(document, modDef, targetInfo, target, - targetParent); - interaction.createParticipation(targetInfo.getDisplayID() + "_" + target.getId(), - targetFC.getIdentity(), getParticipantType(false, interaction.getTypes())); + + // populate sources and targets + if(interactionCell.getStyle().contains(STYLE_INTERACTION_NODE)) { + // multiple sources/targets + mxCell[] interactionEdges = Arrays.stream(mxGraphModel.getEdges(model, interactionCell)).toArray(mxCell[]::new); + for(mxCell interactionEdge : interactionEdges) { + boolean isSource = !interactionEdge.getSource().equals(interactionCell); + addParticipant(document, model, modDef, interaction, isSource, intInfo, interactionEdge); + } + }else { + // single source/target + addParticipant(document, model, modDef, interaction, true, intInfo, interactionCell); + addParticipant(document, model, modDef, interaction, false, intInfo, interactionCell); } - } - } private void linkComponentDefinition(SBOLDocument document, mxGraph graph, mxGraphModel model, @@ -824,7 +804,90 @@ private int getSequenceLength(SBOLDocument document, ComponentDefinition compone } - private FunctionalComponent getOrCreateParticipant(SBOLDocument document, ModuleDefinition modDef, + private void attachEdgeToParticipant(SBOLDocument document, mxGraphModel model, ModuleDefinition modDef, Interaction interaction, boolean isSource, InteractionInfo intInfo, mxCell interactionEdge) throws URISyntaxException, SBOLValidationException { + mxCell participantCell = null; + GlyphInfo participantInfo = null; + if(isSource) { + participantCell = (mxCell) interactionEdge.getSource(); + }else { + participantCell = (mxCell) interactionEdge.getTarget(); + } + + // get the necessary info to generate a participant + if(participantCell != null && participantCell.getStyle().contains(STYLE_MODULE)) { + String subPartURI = null; + if(isSource) { + subPartURI = intInfo.getFromURI().get(interactionEdge.getId()); + } else { + subPartURI = intInfo.getToURI().get(interactionEdge.getId()); + } + String subPartCellID = subPartURI.substring(subPartURI.lastIndexOf("_")+1); + String subPartID = subPartURI.substring(0, subPartURI.lastIndexOf("_")); + participantInfo = (GlyphInfo) infoDict.get(subPartID); + participantCell = (mxCell) model.getCell(subPartCellID); + }else if (participantCell != null) { + participantInfo = (GlyphInfo) infoDict.get(participantCell.getValue()); + } + Set participations = interaction.getParticipations(); + for(Participation participation : participations) { + FunctionalComponent funcComp = participation.getParticipant(); + if(funcComp.getDefinitionURI().equals(new URI(participantInfo.getFullURI()))) { + // probably needs something to identify duplicate parts with separate mapstos, but we're getting close to sbol3 where that shouldn't be a problem + layoutHelper.addGraphicalNode(modDef.getIdentity(), participation.getDisplayId(), interactionEdge); + } + } + } + + private void addParticipant(SBOLDocument document, mxGraphModel model, ModuleDefinition modDef, Interaction interaction, boolean isSource, InteractionInfo intInfo, mxCell interactionEdge) throws SBOLValidationException { + mxCell participantCell = null; + mxCell participantParentCell = null; + GlyphInfo participantInfo = null; + if(isSource) { + participantCell = (mxCell) interactionEdge.getSource(); + }else { + participantCell = (mxCell) interactionEdge.getTarget(); + } + + // get the necessary info to generate a participant + if(participantCell != null && participantCell.getStyle().contains(STYLE_MODULE)) { + String subPartURI = null; + if(isSource) { + subPartURI = intInfo.getFromURI().get(interactionEdge.getId()); + } else { + subPartURI = intInfo.getToURI().get(interactionEdge.getId()); + } + String subPartCellID = subPartURI.substring(subPartURI.lastIndexOf("_")+1); + String subPartID = subPartURI.substring(0, subPartURI.lastIndexOf("_")); + participantInfo = (GlyphInfo) infoDict.get(subPartID); + participantParentCell = participantCell; + participantCell = (mxCell) model.getCell(subPartCellID); + }else if (participantCell != null) { + participantInfo = (GlyphInfo) infoDict.get(participantCell.getValue()); + participantParentCell = (mxCell) participantCell.getParent(); + } + if(participantCell != null) { + FunctionalComponent participantFC = getOrCreateParticipantFC(document, modDef, participantInfo, participantCell, participantParentCell); + URI participantRole = getParticipantType(isSource, interaction.getTypes()); + // extract the role refinment if there is one + if(isSource) { + String refinementName = intInfo.getSourceRefinement().get(interactionEdge.getId()); + if(refinementName != null) + participantRole = SBOLData.getInteractionRoleRefinementFromName(refinementName); + }else { + String refinementName = intInfo.getTargetRefinement().get(interactionEdge.getId()); + if(refinementName != null) + participantRole = SBOLData.getInteractionRoleRefinementFromName(refinementName); + } + // Issue with display id causing duplicate references in layout + Participation participation = interaction.createParticipation(intInfo.getDisplayID()+"_"+interaction.getParticipations().size(), participantFC.getIdentity(), participantRole); + if((isSource && interactionEdge.getTarget() != null && interactionEdge.getTarget().getStyle().contains(STYLE_INTERACTION_NODE)) || + (!isSource && interactionEdge.getSource() != null && interactionEdge.getSource().getStyle().contains(STYLE_INTERACTION_NODE))) { + layoutHelper.addGraphicalNode(modDef.getIdentity(), participation.getDisplayId(), interactionEdge); + } + } + } + + private FunctionalComponent getOrCreateParticipantFC(SBOLDocument document, ModuleDefinition modDef, GlyphInfo partInfo, mxCell part, mxCell parent) throws SBOLValidationException { FunctionalComponent sourceFC = modDef.getFunctionalComponent(partInfo.getDisplayID() + "_" + part.getId()); diff --git a/SBOLCanvasBackend/src/utils/SBOLData.java b/SBOLCanvasBackend/src/utils/SBOLData.java index f821ec9f..6e18f7cc 100644 --- a/SBOLCanvasBackend/src/utils/SBOLData.java +++ b/SBOLCanvasBackend/src/utils/SBOLData.java @@ -8,6 +8,7 @@ import java.util.TreeSet; import org.sbolstandard.core2.ComponentDefinition; +import org.sbolstandard.core2.Participation; import org.sbolstandard.core2.SequenceOntology; import org.sbolstandard.core2.SystemsBiologyOntology; import org.synbiohub.frontend.SynBioHubException; @@ -17,17 +18,22 @@ public class SBOLData { private static SequenceOntology so; + private static SystemsBiologyOntology sbo; public static BiMap types; public static BiMap roles; public static BiMap refinements; public static HashMap parents; public static BiMap interactions; + public static BiMap interactionRoles; + public static HashMap interactionSourceRoles; + public static HashMap interactionTargetRoles; public static HashSet registries; static { so = new SequenceOntology(); + sbo = new SystemsBiologyOntology(); types = new BiMap(); types.put("Complex", ComponentDefinition.COMPLEX); @@ -92,6 +98,37 @@ public class SBOLData { interactions.put("Degradation", SystemsBiologyOntology.DEGRADATION); interactions.put("Genetic Production", SystemsBiologyOntology.GENETIC_PRODUCTION); interactions.put("Control", SystemsBiologyOntology.CONTROL); + interactions.put("Dissociation", SystemsBiologyOntology.DISSOCIATION); + + interactionRoles = new BiMap(); + interactionRoles.put("Inhibitor", SystemsBiologyOntology.INHIBITOR); + interactionRoles.put("Inhibited", SystemsBiologyOntology.INHIBITED); + interactionRoles.put("Stimulator", SystemsBiologyOntology.STIMULATOR); + interactionRoles.put("Stimulated", SystemsBiologyOntology.STIMULATED); + interactionRoles.put("Reactant", SystemsBiologyOntology.REACTANT); + interactionRoles.put("Product", SystemsBiologyOntology.PRODUCT); + interactionRoles.put("Modifier", SystemsBiologyOntology.MODIFIER); + interactionRoles.put("Modified", SystemsBiologyOntology.MODIFIED); + interactionRoles.put("Template", SystemsBiologyOntology.TEMPLATE); + + interactionTargetRoles = new HashMap(); + interactionTargetRoles.put(SystemsBiologyOntology.INHIBITION, SystemsBiologyOntology.INHIBITED); + interactionTargetRoles.put(SystemsBiologyOntology.STIMULATION, SystemsBiologyOntology.STIMULATED); + interactionTargetRoles.put(SystemsBiologyOntology.BIOCHEMICAL_REACTION, SystemsBiologyOntology.PRODUCT); + interactionTargetRoles.put(SystemsBiologyOntology.NON_COVALENT_BINDING, SystemsBiologyOntology.PRODUCT); + interactionTargetRoles.put(SystemsBiologyOntology.GENETIC_PRODUCTION, SystemsBiologyOntology.PRODUCT); + interactionTargetRoles.put(SystemsBiologyOntology.CONTROL, SystemsBiologyOntology.MODIFIED); + interactionTargetRoles.put(SystemsBiologyOntology.DISSOCIATION, SystemsBiologyOntology.PRODUCT); + + interactionSourceRoles = new HashMap(); + interactionSourceRoles.put(SystemsBiologyOntology.INHIBITION, SystemsBiologyOntology.INHIBITOR); + interactionSourceRoles.put(SystemsBiologyOntology.STIMULATION, SystemsBiologyOntology.STIMULATOR); + interactionSourceRoles.put(SystemsBiologyOntology.BIOCHEMICAL_REACTION, SystemsBiologyOntology.REACTANT); + interactionSourceRoles.put(SystemsBiologyOntology.NON_COVALENT_BINDING, SystemsBiologyOntology.REACTANT); + interactionSourceRoles.put(SystemsBiologyOntology.DEGRADATION, SystemsBiologyOntology.REACTANT); + interactionSourceRoles.put(SystemsBiologyOntology.GENETIC_PRODUCTION, SystemsBiologyOntology.REACTANT); + interactionSourceRoles.put(SystemsBiologyOntology.CONTROL, SystemsBiologyOntology.MODIFIER); + interactionSourceRoles.put(SystemsBiologyOntology.DISSOCIATION, SystemsBiologyOntology.REACTANT); registries = new HashSet(); try { @@ -104,18 +141,31 @@ public class SBOLData { } + /** + * Returns the top type names for component definitions. + * @return + */ public static String[] getTypes() { String[] typeNames = types.keys().toArray(new String[0]); Arrays.sort(typeNames); return typeNames; } + /** + * Returns the top role names for component definitions. + * @return + */ public static String[] getRoles() { String[] roleNames = roles.keys().toArray(new String[0]); Arrays.sort(roleNames); return roleNames; } + /** + * Returns all children SO terms of the parent SO term. + * @param parentName - The parent name to find all children of + * @return + */ public static String[] getRefinement(String parentName){ if(parentName == null || parentName.equals("")) { parentName = "NGA (No Glyph Assigned)"; @@ -128,10 +178,68 @@ public static String[] getRefinement(String parentName){ return refinementNames.toArray(new String[0]); } + /** + * Returns all children SBO terms of the parent SBO term. + * @param parentName - The parent name to find all children of + * @return + */ + public static String[] getInteractionRoleRefinement(String parentName) { + if(parentName == null || parentName.contentEquals("")) { + return new String[0]; + } + URI parentURI = interactionRoles.getValue(parentName); + if(parentURI == null) { + return new String[0]; + } + Set refinementNames = new TreeSet(); + Set descendants = sbo.getDescendantURIsOf(parentURI); + for(URI uri : descendants) { + refinementNames.add(sbo.getName(uri)); + } + return refinementNames.toArray(new String[0]); + } + + /** + * Returns a sorted list of interaction names + */ public static String[] getInteractions() { String[] interactionNames = interactions.keys().toArray(new String[0]); Arrays.sort(interactionNames); return interactionNames; } + + /** + * Returns a hashmap of interaction name to valid roles (roles[0] = source role, roles[1] = target role) + */ + public static HashMap getInteractionRoles(){ + HashMap result = new HashMap(); + for(URI interactionURI : interactions.values()) { + String[] roles = new String[2]; + roles[0] = interactionRoles.getKey(interactionSourceRoles.get(interactionURI)); + roles[1] = interactionRoles.getKey(interactionTargetRoles.get(interactionURI)); + result.put(interactions.getKey(interactionURI), roles); + } + return result; + } + + public static URI getInteractionRoleRefinementFromName(String name) { + return sbo.getURIbyName(name); + } + + public static String getInteractionRoleRefinementName(URI refinement) { + return sbo.getName(refinement); + } + + public static boolean isSourceParticipant(Participation participant) { + for(URI sourceRole : interactionSourceRoles.values()) { + Set participantRoles = participant.getRoles(); + for(URI partRole : participantRoles) { + if(partRole.equals(sourceRole) || sbo.isDescendantOf(partRole, sourceRole)) { + return true; + } + } + } + return false; + } } diff --git a/SBOLCanvasBackend/src/utils/SBOLToMx.java b/SBOLCanvasBackend/src/utils/SBOLToMx.java index bc027158..3c1d799c 100644 --- a/SBOLCanvasBackend/src/utils/SBOLToMx.java +++ b/SBOLCanvasBackend/src/utils/SBOLToMx.java @@ -74,6 +74,7 @@ public class SBOLToMx extends Converter { public SBOLToMx() { infoDict = new Hashtable(); combinatorialDict = new Hashtable(); + interactionDict = new Hashtable(); compToCell = new HashMap(); mappings = new HashMap(); } @@ -106,6 +107,7 @@ public void toGraph(SBOLDocument document, SBOLDocument combDocument, OutputStre ArrayList dataContainer = new ArrayList(); dataContainer.add(INFO_DICT_INDEX, infoDict); dataContainer.add(COMBINATORIAL_DICT_INDEX, combinatorialDict); + dataContainer.add(INTERACTION_DICT_INDEX, interactionDict); cell0.setValue(dataContainer); layoutHelper = new LayoutHelper(document, graph); @@ -415,41 +417,87 @@ private void setupModuleInteractions(SBOLDocument document, mxGraph graph, Modul // interactions Set interactions = modDef.getInteractions(); for (Interaction interaction : interactions) { - mxCell edge = layoutHelper.getGraphicalObject(modDef.getIdentity(), interaction.getDisplayId()); - if (edge != null) { - if (edge.getStyle() != null) - edge.setStyle(STYLE_INTERACTION + ";" + edge.getStyle()); + Participation[] participations = interaction.getParticipations().toArray(new Participation[0]); + boolean hasNode = participations.length > 2; + mxCell interactionCell = layoutHelper.getGraphicalObject(modDef.getIdentity(), interaction.getDisplayId()); + if (interactionCell != null) { + if (interactionCell.getStyle() != null) + if (hasNode) + interactionCell.setStyle(STYLE_INTERACTION_NODE + ";" + interactionCell.getStyle()); + else + interactionCell.setStyle(STYLE_INTERACTION + ";" + interactionCell.getStyle()); + else if (hasNode) + interactionCell.setStyle(STYLE_INTERACTION_NODE); else - edge.setStyle(STYLE_INTERACTION); - edge = (mxCell) model.add(rootViewCell, edge, 0); + interactionCell.setStyle(STYLE_INTERACTION); + interactionCell = (mxCell) model.add(rootViewCell, interactionCell, 0); } else { - edge = (mxCell) graph.insertEdge(rootViewCell, null, null, null, null); + if (hasNode) + interactionCell = (mxCell) graph.insertVertex(rootViewCell, null, null, 0, 0, 0, 0, STYLE_INTERACTION_NODE); + else + interactionCell = (mxCell) graph.insertEdge(rootViewCell, null, null, null, null); } - edge.setValue(genInteractionInfo(interaction)); - - URI targetType = getParticipantType(false, interaction.getTypes()); - URI sourceType = getParticipantType(true, interaction.getTypes()); - - Participation[] participations = interaction.getParticipations().toArray(new Participation[0]); - for (int i = 0; i < participations.length; i++) { - // theoretically more than 2, but we currently only support 2 - if (participations[i].getRoles().contains(sourceType)) { - mxCell source = compToCell.get(participations[i].getParticipant()); - edge.setSource(source); - if (source.getStyle().contains(STYLE_MODULE)) { - mxCell referenced = compToCell.get(mappings.get(participations[i].getParticipant())); - ((InteractionInfo) edge.getValue()) - .setFromURI(referenced.getValue() + "_" + referenced.getId()); + InteractionInfo intInfo = genInteractionInfo(interaction); + interactionDict.put(intInfo.getFullURI(), intInfo); + interactionCell.setValue(intInfo.getFullURI()); + + for(Participation participation : participations) { + // determine if the participation is a source or target + boolean source = SBOLData.isSourceParticipant(participation); + // pull the interaction edge from the participation if connected to an interaction node or create a new one + if(hasNode) { + mxCell interactionEdge = layoutHelper.getGraphicalObject(modDef.getIdentity(), participation.getDisplayId()); + if (interactionEdge != null) { + if (interactionEdge.getStyle() != null) + interactionEdge.setStyle(STYLE_INTERACTION + ";" + interactionEdge.getStyle()); + else + interactionEdge.setStyle(STYLE_INTERACTION); + interactionEdge = (mxCell) model.add(rootViewCell, interactionEdge, 0); + } else { + interactionEdge = (mxCell) graph.insertEdge(rootViewCell, null, null, null, null); } - } else if (participations[i].getRoles().contains(targetType)) { - mxCell target = compToCell.get(participations[i].getParticipant()); - edge.setTarget(target); - if (target.getStyle().contains(STYLE_MODULE)) { - mxCell referenced = compToCell.get(mappings.get(participations[i].getParticipant())); - ((InteractionInfo) edge.getValue()).setToURI(referenced.getValue() + "_" + referenced.getId()); + interactionEdge.setValue(intInfo.getFullURI()); + setInteractionEndpoints(document, interaction, participation, source, interactionEdge); + if(source) { + interactionEdge.setTarget(interactionCell); + }else { + interactionEdge.setSource(interactionCell); } + }else{ + setInteractionEndpoints(document, interaction, participation, source, interactionCell); } } + + } + } + + private void setInteractionEndpoints(SBOLDocument document, Interaction interaction, Participation participation, + boolean source, mxCell interactionEdge) { + URI endpointType = getParticipantType(source, interaction.getTypes()); + + mxCell endpoint = compToCell.get(participation.getParticipant()); + InteractionInfo intInfo = interactionDict.get(interactionEdge.getValue()); + // set the cell source/target + if(source) + interactionEdge.setSource(endpoint); + else + interactionEdge.setTarget(endpoint); + // set the source/target refinement + if(!participation.getRoles().contains(endpointType)) { + // take the first one as the refinement + URI partRefinement = participation.getRoles().toArray(new URI[0])[0]; + if(source) + intInfo.getSourceRefinement().put(interactionEdge.getId(), SBOLData.getInteractionRoleRefinementName(partRefinement)); + else + intInfo.getTargetRefinement().put(interactionEdge.getId(), SBOLData.getInteractionRoleRefinementName(partRefinement)); + } + // set the to/fromURI if needed + if (endpoint.getStyle().contains(STYLE_MODULE)) { + mxCell referenced = compToCell.get(mappings.get(participation.getParticipant())); + if(source) + intInfo.getFromURI().put(interactionEdge.getId(), referenced.getValue()+"_"+referenced.getId()); + else + intInfo.getToURI().put(interactionEdge.getId(), referenced.getValue()+"_"+referenced.getId()); } } @@ -531,6 +579,7 @@ private InteractionInfo genInteractionInfo(Interaction interaction) { InteractionInfo info = new InteractionInfo(); info.setDisplayID(interaction.getDisplayId()); info.setInteractionType(SBOLData.interactions.getKey(interaction.getTypes().iterator().next())); + info.setUriPrefix(getURIPrefix(interaction)); return info; } @@ -585,7 +634,7 @@ private VariableComponentInfo genVariableComponentInfo(mxGraph graph, mxGraphMod mxCell[] containerChildren = Arrays.stream(mxGraphModel.filterCells(viewChildren, containerFilter)) .toArray(mxCell[]::new); mxCell container = containerChildren[0]; - mxCell compCell = (mxCell) container.getChildAt(index+1); // add one to offset from the backbone + mxCell compCell = (mxCell) container.getChildAt(index + 1); // add one to offset from the backbone varCompInfo.setCellID(compCell.getId()); ArrayList variants = new ArrayList(); diff --git a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html index 7c09352d..67b57a2d 100644 --- a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html +++ b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.html @@ -79,7 +79,7 @@ - + diff --git a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts index 03a630c4..4ebad558 100644 --- a/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts +++ b/SBOLCanvasFrontend/src/app/glyph-menu/glyph-menu.component.ts @@ -7,10 +7,8 @@ import {Component, OnInit, AfterViewInit, ViewChildren, QueryList, ElementRef, ViewEncapsulation} from '@angular/core'; import {GraphService} from '../graph.service'; import {GlyphService} from '../glyph.service'; -import {SearchfilterPipe} from '../searchfilter.pipe'; import {DomSanitizer} from '@angular/platform-browser'; import {MetadataService} from '../metadata.service'; -import {register} from 'ts-node'; @Component({ selector: 'app-glyph-menu', @@ -27,6 +25,7 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { SEQUENCE_FEATURE: "Sequence Feature", MOLECULAR_SPECIES: "Molecular Species", INTERACTION: "Interaction", + INTERACTION_NODE: "Interaction Node", } @ViewChildren('canvasElement') canvasElements: QueryList; @@ -53,8 +52,8 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { } onInteractionNodeGlyphClicked(name: string){ - // name = name.charAt(0).toUpperCase()+name.slice(1); - // this.graphService.addInteractionNode(name); + //name = name.charAt(0).toUpperCase()+name.slice(1); + this.graphService.addInteractionNode(name); } onInteractionGlyphClicked(name: string) { @@ -102,6 +101,9 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { case this.elementTypes.INTERACTION: this.graphService.makeInteractionDragsource(elt, elt.getAttribute('glyphStyle')); break; + case this.elementTypes.INTERACTION_NODE: + this.graphService.makeInteractionNodeDragsource(elt, elt.getAttribute('glyphStyle')); + break; } } @@ -126,6 +128,8 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { this.utilsDict[name] = this.sanitizer.bypassSecurityTrustHtml(svg.innerHTML); } for (const name in molecularSpeciesElts) { + if(name == "replacement-glyph") // Shouldn't be possible to add in canvas, only loaded + continue; const svg = molecularSpeciesElts[name]; this.molecularSpeciesDict[name] = this.sanitizer.bypassSecurityTrustHtml(svg.innerHTML); } @@ -134,6 +138,8 @@ export class GlyphMenuComponent implements OnInit, AfterViewInit { this.interactionsDict[name] = this.sanitizer.bypassSecurityTrustHtml(svg.innerHTML); } for(const name in interactionNodeElts){ + if(name == "replacement-glyph") // Shouldn't be possible to add in canvas, only loaded + continue; const svg = interactionNodeElts[name]; this.interactionNodeDict[name] = this.sanitizer.bypassSecurityTrustHtml(svg.innerHTML); } diff --git a/SBOLCanvasFrontend/src/app/glyph.service.ts b/SBOLCanvasFrontend/src/app/glyph.service.ts index baabe81f..ed29698a 100644 --- a/SBOLCanvasFrontend/src/app/glyph.service.ts +++ b/SBOLCanvasFrontend/src/app/glyph.service.ts @@ -60,6 +60,7 @@ export class GlyphService { 'assets/glyph_stencils/molecular_species/small-molecule.xml', 'assets/glyph_stencils/molecular_species/no-glyph-assigned-ms.xml', 'assets/glyph_stencils/molecular_species/replacement-glyph.xml', + 'assets/glyph_stencils/molecular_species/complex.xml', ]; private interactionXMLs: string[] = [ @@ -74,6 +75,7 @@ export class GlyphService { 'assets/glyph_stencils/interaction_nodes/association.xml', 'assets/glyph_stencils/interaction_nodes/dissociation.xml', 'assets/glyph_stencils/interaction_nodes/process.xml', + 'assets/glyph_stencils/molecular_species/replacement-glyph.xml', ] private indicatorXMLs: string[] = [ diff --git a/SBOLCanvasFrontend/src/app/graph-base.ts b/SBOLCanvasFrontend/src/app/graph-base.ts index b99841ad..bfd23e64 100644 --- a/SBOLCanvasFrontend/src/app/graph-base.ts +++ b/SBOLCanvasFrontend/src/app/graph-base.ts @@ -7,11 +7,9 @@ import { GlyphService } from './glyph.service'; import { CanvasAnnotation } from './canvasAnnotation'; import { environment } from 'src/environments/environment'; import { ModuleInfo } from './moduleInfo'; -import { GraphEdits } from './graph-edits'; import { CombinatorialInfo } from './combinatorialInfo'; import { VariableComponentInfo } from './variableComponentInfo'; import { IdentifiedInfo } from './identifiedInfo'; -import { mxShape } from 'src/mxgraph'; import { CustomShapes } from './CustomShapes'; // mx is used here as the typings file for mxgraph isn't up to date. @@ -31,6 +29,7 @@ export class GraphBase { // Constants static readonly INFO_DICT_INDEX = 0; static readonly COMBINATORIAL_DICT_INDEX = 1; + static readonly INTERACTION_DICT_INDEX = 2; static readonly sequenceFeatureGlyphWidth = 50; static readonly sequenceFeatureGlyphHeight = 100; @@ -39,6 +38,9 @@ export class GraphBase { static readonly molecularSpeciesGlyphWidth = 50; static readonly molecularSpeciesGlyphHeight = 50; + static readonly interactionNodeGlyphWidth = 50; + static readonly interactionNodeGlyphHeight = 50; + static readonly defaultTextWidth = 120; static readonly defaultTextHeight = 80; @@ -56,6 +58,7 @@ export class GraphBase { static readonly STYLE_MOLECULAR_SPECIES = 'molecularSpeciesGlyph'; static readonly STYLE_SEQUENCE_FEATURE = 'sequenceFeatureGlyph'; static readonly STYLE_INTERACTION = 'interactionGlyph'; + static readonly STYLE_INTERACTION_NODE = 'interactionNodeGlyph'; static readonly STYLE_MODULE_VIEW = "moduleViewCell"; static readonly STYLE_COMPONENT_VIEW = "componentViewCell"; static readonly STYLE_INDICATOR = "indicator"; @@ -81,6 +84,7 @@ export class GraphBase { baseMolecularSpeciesGlyphStyle: any; baseSequenceFeatureGlyphStyle: any; + baseInteractionNodeGlyphStyle: any; // This object handles the hotkeys for the graph. keyHandler: any; @@ -141,6 +145,7 @@ export class GraphBase { this.initStyles(); this.initCustomShapes(); this.initListeners(); + this.initEdgeValidation(); } /** @@ -153,6 +158,8 @@ export class GraphBase { window['mxPoint'] = mx.mxPoint; window['mxCell'] = mx.mxCell; + let graphBaseRef = this; // for use in overide methods where you need one of the helpers here + let genericDecode = function (dec, node, into) { const meta = node; if (meta != null) { @@ -187,11 +194,11 @@ export class GraphBase { // Module info encode/decode Object.defineProperty(ModuleInfo, "name", { configurable: true, value: "ModuleInfo" }); const moduleInfoCodec = new mx.mxObjectCodec(new ModuleInfo()); - moduleInfoCodec.decode = function(dec, node, into){ + moduleInfoCodec.decode = function (dec, node, into) { const moduleData = new ModuleInfo(); return genericDecode(dec, node, moduleData); } - moduleInfoCodec.encode = function(enc, object){ + moduleInfoCodec.encode = function (enc, object) { return object.encode(enc); } mx.mxCodecRegistry.register(moduleInfoCodec); @@ -211,31 +218,31 @@ export class GraphBase { window['InteractionInfo'] = InteractionInfo; // combinatorial info decode - Object.defineProperty(CombinatorialInfo, "name", { configurable: true, value: "CombinatorialInfo"}); + Object.defineProperty(CombinatorialInfo, "name", { configurable: true, value: "CombinatorialInfo" }); const combinatorialInfoCodec = new mx.mxObjectCodec(new CombinatorialInfo()); - combinatorialInfoCodec.decode = function (dec, node, into){ + combinatorialInfoCodec.decode = function (dec, node, into) { const combinatorialData = new CombinatorialInfo(); return genericDecode(dec, node, combinatorialData); } - combinatorialInfoCodec.encode = function (enc, object){ + combinatorialInfoCodec.encode = function (enc, object) { return object.encode(enc); } mx.mxCodecRegistry.register(combinatorialInfoCodec); window['CombinatorialInfo'] = CombinatorialInfo; // variable component info decode - Object.defineProperty(VariableComponentInfo, "name", { configurable: true, value: "VariableComponentInfo"}); + Object.defineProperty(VariableComponentInfo, "name", { configurable: true, value: "VariableComponentInfo" }); const variableComponentInfoCodec = new mx.mxObjectCodec(new VariableComponentInfo()); - variableComponentInfoCodec.decode = function (dec, node, into){ + variableComponentInfoCodec.decode = function (dec, node, into) { const variableComponentData = new VariableComponentInfo(); return genericDecode(dec, node, variableComponentData); } mx.mxCodecRegistry.register(variableComponentInfoCodec); window['VariableComponentInfo'] = VariableComponentInfo; - Object.defineProperty(IdentifiedInfo, "name", { configurable: true, value: "IdentifiedInfo"}); + Object.defineProperty(IdentifiedInfo, "name", { configurable: true, value: "IdentifiedInfo" }); const identifiedInfoCodec = new mx.mxObjectCodec(new IdentifiedInfo()); - identifiedInfoCodec.decode = function (dec, node, into){ + identifiedInfoCodec.decode = function (dec, node, into) { const identifiedData = new IdentifiedInfo(); return genericDecode(dec, node, identifiedData); } @@ -267,9 +274,10 @@ export class GraphBase { cell0 = cell0.parent; } let glyphDict = cell0.value[GraphBase.INFO_DICT_INDEX]; + let interactionDict = cell0.value[GraphBase.INTERACTION_DICT_INDEX]; // check for format conditions - if (((cell.isCircuitContainer() && cell.getParent().isModuleView()) || cell.isMolecularSpeciesGlyph() || cell.isModule()) && cell.getGeometry().height == 0) { + if (((cell.isCircuitContainer() && cell.getParent().isModuleView()) || cell.isMolecularSpeciesGlyph() || cell.isModule() || cell.isInteractionNode()) && cell.getGeometry().height == 0) { GraphBase.unFormatedCells.add(cell.getParent().getId()); } @@ -283,12 +291,23 @@ 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+";")) + reconstructCellStyle = true; + 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].partType === 'DNA region') { + if(glyphDict[cell.value] instanceof ModuleInfo){ + // module + if(!cell.style){ + cell.style = GraphBase.STYLE_MODULE; + } + cell.geometry.width = GraphBase.defaultModuleWidth; + cell.geometry.height = GraphBase.defaultModuleHeight; + } else if (glyphDict[cell.value] instanceof GlyphInfo && glyphDict[cell.value].partType === 'DNA region') { // sequence feature if (!cell.style) { cell.style = GraphBase.STYLE_SEQUENCE_FEATURE + glyphDict[cell.value].partRole; @@ -299,7 +318,7 @@ export class GraphBase { cell.geometry.width = GraphBase.sequenceFeatureGlyphWidth; if (cell.geometry.height == 0) cell.geometry.height = GraphBase.sequenceFeatureGlyphHeight; - } else { + } else if(glyphDict[cell.value] instanceof GlyphInfo){ // molecular species if (!cell.style) cell.style = GraphBase.STYLE_MOLECULAR_SPECIES + "macromolecule"; @@ -308,16 +327,29 @@ export class GraphBase { cell.geometry.width = GraphBase.molecularSpeciesGlyphWidth; cell.geometry.height = GraphBase.molecularSpeciesGlyphHeight; } - } else if (cell.value instanceof InteractionInfo) { - // interaction - let name = cell.value.interactionType; - if (name == "Biochemical Reaction" || name == "Non-Covalent Binding" || name == "Genetic Production") { - name = "Process"; - } - if (!cell.style) { - cell.style = GraphBase.STYLE_INTERACTION + name; - } else { - cell.style = cell.style.replace(GraphBase.STYLE_INTERACTION, GraphBase.STYLE_INTERACTION + name); + } else if (interactionDict[cell.value] != null) { + let intInfo = interactionDict[cell.value]; + if(cell.isVertex()){ + // interaction node + let name = graphBaseRef.interactionNodeTypeToName(intInfo.interactionType); + 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{ + // interaction + let name = intInfo.interactionType; + if (name == "Biochemical Reaction" || name == "Non-Covalent Binding" || name == "Genetic Production") { + name = "Process"; + } + if (!cell.style) { + cell.style = GraphBase.STYLE_INTERACTION + name; + } else { + cell.style = cell.style.replace(GraphBase.STYLE_INTERACTION, GraphBase.STYLE_INTERACTION + name); + } } } } @@ -362,6 +394,10 @@ export class GraphBase { return this.isStyle(GraphBase.STYLE_SCAR); }; + mx.mxCell.prototype.isInteractionNode = function () { + return this.isStyle(GraphBase.STYLE_INTERACTION_NODE); + } + mx.mxCell.prototype.isInteraction = function () { return this.isStyle(GraphBase.STYLE_INTERACTION); } @@ -642,6 +678,8 @@ export class GraphBase { this.baseSequenceFeatureGlyphStyle = mx.mxUtils.clone(this.baseMolecularSpeciesGlyphStyle); this.baseSequenceFeatureGlyphStyle[mx.mxConstants.STYLE_PORT_CONSTRAINT] = [mx.mxConstants.DIRECTION_NORTH, mx.mxConstants.DIRECTION_SOUTH]; + this.baseInteractionNodeGlyphStyle = mx.mxUtils.clone(this.baseMolecularSpeciesGlyphStyle); + const textBoxStyle = {}; textBoxStyle[mx.mxConstants.STYLE_SHAPE] = mx.mxConstants.SHAPE_LABEL; textBoxStyle[mx.mxConstants.STYLE_FILLCOLOR] = '#ffffff'; @@ -709,18 +747,18 @@ export class GraphBase { this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_INTERACTION + GraphBase.interactionDegradationName, interactionDegradationStyle); // vertex selection border styles - mx.mxVertexHandler.prototype.getSelectionColor = function() { - if(this.state.cell.style.startsWith(GraphBase.STYLE_CIRCUIT_CONTAINER)){ + mx.mxVertexHandler.prototype.getSelectionColor = function () { + if (this.state.cell.style.startsWith(GraphBase.STYLE_CIRCUIT_CONTAINER)) { // circuit container selection color return '#0000ff'; - }else{ + } else { // default color return '#00aa00'; } } // edge selection border styles - mx.mxEdgeHandler.prototype.getSelectionColor = function() { + mx.mxEdgeHandler.prototype.getSelectionColor = function () { return '#00aa00'; } } @@ -737,17 +775,17 @@ export class GraphBase { // we need this if we intend on creating custom shapes with stencils let sequenceFeatureStencils = this.glyphService.getSequenceFeatureGlyphs(); - mx.mxCellRenderer.prototype.createShape = function(state){ + mx.mxCellRenderer.prototype.createShape = function (state) { var shape = null; - if(state.style != 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) { shape = new CustomShapes.SequenceFeatureShape(stencil); - }else if(stencil != null){ + } else if (stencil != null) { shape = new mx.mxShape(stencil); - }else{ + } else { var ctor = this.getShapeConstructor(state); shape = new ctor(); } @@ -775,14 +813,14 @@ export class GraphBase { y += h / 2; origDrawShape.apply(this, [canvas, shape, x, y, w, h]); - shape.paintComposite(canvas, x, y-(h/2), w, h*2); + 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); } } @@ -807,16 +845,28 @@ export class GraphBase { this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_MOLECULAR_SPECIES + name, newGlyphStyle); } + // interaction nodes are basically identical to molecular species + stencils = this.glyphService.getInteractionNodeGlyphs(); + for (const name in stencils) { + const stencil = stencils[name][0]; + let customStencil = new mx.mxStencil(stencil.desc); + mx.mxStencilRegistry.addStencil(name, customStencil); + + const newGlyphStyle = mx.mxUtils.clone(this.baseInteractionNodeGlyphStyle); + newGlyphStyle[mx.mxConstants.STYLE_SHAPE] = name; + this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_INTERACTION_NODE + name, newGlyphStyle); + } + // indicators like composit and combinatorial stencils = this.glyphService.getIndicatorGlyphs(); - for(const name in stencils){ + for (const name in stencils) { const stencil = stencils[name][0]; let customStencil = new mx.mxStencil(stencil.desc); mx.mxStencilRegistry.addStencil(name, customStencil); const newIndicatorStyle = mx.mxUtils.clone(this.baseMolecularSpeciesGlyphStyle); newIndicatorStyle[mx.mxConstants.STYLE_SHAPE] = name; - this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_INDICATOR+name, newIndicatorStyle); + this.graph.getStylesheet().putCellStyle(GraphBase.STYLE_INDICATOR + name, newIndicatorStyle); } // *** Define custom markers for edge endpoints *** @@ -886,10 +936,10 @@ export class GraphBase { mx.mxMarker.addMarker(GraphBase.interactionDegradationName, degradationMarkerDrawFunction); let oldGetIndicatorShape = mx.mxGraph.prototype.getIndicatorShape; - mx.mxGraph.prototype.getIndicatorShape = function (state){ - if(state.cell.isSequenceFeatureGlyph()){ + mx.mxGraph.prototype.getIndicatorShape = function (state) { + if (state.cell.isSequenceFeatureGlyph()) { return 'composite'; - }else{ + } else { return oldGetIndicatorShape(state); } } @@ -900,37 +950,109 @@ export class GraphBase { */ initListeners() { // edge movement - this.graph.addListener(mx.mxEvent.CONNECT_CELL, mx.mxUtils.bind(this, async function(sender, evt){ - - // if the terminal is a module, we need to prompt what it should be changed to, otherwise just clear it - + this.graph.addListener(mx.mxEvent.CONNECT_CELL, mx.mxUtils.bind(this, async function (sender, evt) { let edge = evt.getProperty("edge"); - let terminal = evt.getProperty("terminal"); - let source = evt.getProperty("source"); + let terminal = evt.getProperty("terminal"); // The cell that's either source or dest + let previous = evt.getProperty("previous"); // The previous terminal cell + let source = evt.getProperty("source"); // boolean, true if terminal is the new source let cancelled = false; - try{ + try { sender.getModel().beginUpdate(); + // new terminal is a module, prompt for the sub part to keep track of let newTarget = null; - if(terminal != null && terminal.isModule()){ + if (terminal != null && terminal.isModule()) { newTarget = await this.promptChooseFunctionalComponent(terminal, source); - if(!newTarget){ + if (!newTarget) { cancelled = true; return; } } - let infoCopy = edge.value.makeCopy(); + let infoCopy = this.getFromInteractionDict(edge.value).makeCopy(); + + // previous terminal was an interaction node, we need to decouple from it + if (previous && previous.isInteractionNode()) { + // remove any refinements with this edge from original + let sourceRefinement = infoCopy.sourceRefinement[edge.getId()]; + let targetRefinement = infoCopy.targetRefinement[edge.getId()]; + delete infoCopy.sourceRefinement[edge.getId()]; + delete infoCopy.targetRefinement[edge.getId()]; + this.updateInteractionDict(infoCopy); + infoCopy = infoCopy.makeCopy(); // shouldn't modify the original copy anymore, or we mess up the history + + // remove all refinements from the copy + infoCopy.sourceRefinement = {}; + infoCopy.targetRefinement = {}; + + // add back refinements relating to ours + if(sourceRefinement){ + infoCopy.sourceRefinement[edge.getId()] = sourceRefinement; + } + if(targetRefinement){ + infoCopy.targetRefinement[edge.getId()] = targetRefinement; + } + + // make a dummy info so we can steal it's id + let dummyInfo = new InteractionInfo(); + infoCopy.displayID = dummyInfo.displayID; + // update the edges reference + this.graph.getModel().setValue(edge, infoCopy.getFullURI()); + } + + // if the previous terminal was a module, we need to remove it's to/fromURI + if (previous && previous.isModule()) { + if (source) { + delete infoCopy.fromURI[edge.getId()]; + } else { + delete infoCopy.toURI[edge.getId()]; + } + } + + // new terminal is an interaction node, we need to couple with it + if (terminal && terminal.isInteractionNode()) { + 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()]){ + nodeInfo.fromURI[edge.getId()] = infoCopy.fromURI[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){ + nodeInfo.sourceRefinement[edge.getId()] = sourceRefinement; + } + let targetRefinement = infoCopy.targetRefinement[edge.getId()]; + 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()){ + this.removeFromInteractionDict(oldURI); + } - if(source){ - infoCopy.fromURI = newTarget; - }else{ - infoCopy.toURI = newTarget; + infoCopy = nodeInfo; } - sender.getModel().execute(new GraphEdits.interactionEdit(edge, infoCopy)); - }finally{ + if (newTarget) { + if (source) { + infoCopy.fromURI[edge.getId()] = newTarget; + } else { + infoCopy.toURI[edge.getId()] = newTarget; + } + } + + this.updateInteractionDict(infoCopy); + } finally { sender.getModel().endUpdate(); // undo has to happen after end update if (cancelled) { @@ -939,6 +1061,8 @@ export class GraphBase { } } evt.consume(); + + this.updateAngularMetadata(this.graph.getSelectionCells()); })); // cell movement @@ -1069,6 +1193,94 @@ export class GraphBase { })); } + /** + * Overrides methods necessary to prevent edge connections under certain conditions. + */ + protected initEdgeValidation() { + // We have to override this method because multiplicities only are checked when there is a source and target. + // Multiplicities also base their type on cell.value, not cell.style + let oldGetEdgeValidationError = mx.mxGraph.prototype.getEdgeValidationError; + let validateInteractionRef = this.validateInteraction; + mx.mxGraph.prototype.getEdgeValidationError = function (edge, source, target) { + let result = oldGetEdgeValidationError.apply(this, arguments); + + // will only be null if there wasn't already a condition preventing a connection + if (result != null) { + return result; + } + + let styleString = edge.style.slice(); + 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); + + let validationMessage = validateInteractionRef(interactionType, source, target); + + return validationMessage; + } + } + + protected validateInteraction(interactionType: string, source: mxCell, target: mxCell) { + + // edges can't connect to edges + if((source && source.isEdge()) || (target && target.isEdge())){ + return "Edges are dissallowed to connect to edges."; + } + + // certain edge types can't connect to interaction nodes + if (((source && source.isInteractionNode()) || (target && target.isInteractionNode())) && + (interactionType == 'Control' || interactionType == 'Inhibition' || interactionType == 'Stimulation')) { + return 'Edge type dissallowed to connect to an interaction node.'; + } + + // prevent degredation from using anything as a target + if (interactionType == 'Degradation' && target) { + return 'Degradation isn\'t allowed target anything.'; + } + + // prevent degredation from having anything but a molecular species as a source + if (interactionType == 'Degradation' && source && !source.isMolecularSpeciesGlyph()) { + return 'Degredation is only allowed molecular species as a source.'; + } + + // prevent interaction nodes from chaining + if (source && target && source.isInteractionNode() && target.isInteractionNode()) { + return 'Interaction nodes aren\'t allowed to connect.'; + } + + return null; + } + + protected interactionNodeNametoType(name: string) { + switch (name) { + case "association": + return "Non-Covalent Binding"; + case "dissociation": + return "Dissociation"; + case "process": + return "Process"; + } + } + + protected interactionNodeTypeToName(type: string) { + switch (type) { + case "Non-Covalent Binding": + return "association"; + case "Dissociation": + return "dissociation"; + case "Inhibition": + case "Stimulation": + case "Biochemical Reaction": + case "Degradation": + case "Genetic Production": + case "Control": + return "process"; + default: + return "unkown"; + } + } + protected moleculeNameToType(name: string) { switch (name) { case "dsNA": @@ -1083,6 +1295,8 @@ export class GraphBase { return "RNA molecule"; case "replacement-glyph": return "All_types"; + case "complex": + return "Complex"; default: return "Protein"; } @@ -1102,6 +1316,8 @@ export class GraphBase { return "ssNA"; case "All_types": return "replacement-glyph"; + case "Complex": + return "complex"; default: return "NGA (No Glyph Assigned Molecular Species)"; } diff --git a/SBOLCanvasFrontend/src/app/graph-helpers.ts b/SBOLCanvasFrontend/src/app/graph-helpers.ts index b124430a..9fcfa5ed 100644 --- a/SBOLCanvasFrontend/src/app/graph-helpers.ts +++ b/SBOLCanvasFrontend/src/app/graph-helpers.ts @@ -20,6 +20,8 @@ import { FuncCompSelectorComponent } from './func-comp-selector/func-comp-select import { CombinatorialInfo } from './combinatorialInfo'; import { VariableComponentInfo } from './variableComponentInfo'; import { IdentifiedInfo } from './identifiedInfo'; +import { InteractionInfo } from './interactionInfo'; +import { SystemJsNgModuleLoader } from '@angular/core'; /** * Extension of the graph base that should contain helper methods to be used in the GraphService. @@ -36,9 +38,11 @@ export class GraphHelpers extends GraphBase { const cell0 = this.graph.getModel().getCell(0); const infoDict = []; const combinatorialDict = []; + const interactionDict = []; var dataContainer = []; dataContainer[GraphBase.INFO_DICT_INDEX] = infoDict; dataContainer[GraphBase.COMBINATORIAL_DICT_INDEX] = combinatorialDict; + dataContainer[GraphBase.INTERACTION_DICT_INDEX] = interactionDict; this.graph.getModel().setValue(cell0, dataContainer); // initalize the root view cell of the graph @@ -310,7 +314,7 @@ export class GraphHelpers extends GraphBase { break; } } - if(circuitContainer != null){ + if (circuitContainer != null) { this.syncCircuitContainer(circuitContainer); } } @@ -643,7 +647,7 @@ export class GraphHelpers extends GraphBase { // check if we already have a link and break if we do let found = false; let varCompInfo = combinatorial.getVariableComponentInfo(this.selectionStack[i].getId()); - if(!varCompInfo){ + if (!varCompInfo) { // If the variable component didn't exist, we need to add one varCompInfo = new VariableComponentInfo(this.selectionStack[i].getId()); combinatorial.addVariableComponentInfo(varCompInfo); @@ -656,7 +660,7 @@ export class GraphHelpers extends GraphBase { break; } } - if(found){ + if (found) { // if it did link, update the info, we assume that the link works all the way up the chain variant.description = previous.description; variant.displayId = previous.displayID; @@ -664,7 +668,7 @@ export class GraphHelpers extends GraphBase { variant.uri = previous.getFullURI(); variant.version = previous.version; break; - }else{ + } else { // if it wasn't found we need to add it let variant = new IdentifiedInfo(); variant.description = previous.description; @@ -689,6 +693,64 @@ export class GraphHelpers extends GraphBase { } } + protected updateSelectedInteractionInfo(this: GraphService, info: InteractionInfo) { + let selectedCells = this.graph.getSelectionCells(); + if (selectedCells.length > 1 || selectedCells.length < 0) { + console.error("Trying to change info on too many or too few cells!"); + return; + } + let selectedCell = selectedCells[0]; + + // gather all the cells referencing this interaction + let cell1 = this.graph.getModel().getCell("1"); + let cells = []; + for (let viewChild of cell1.children) { + if (viewChild.children) { + for (let child of viewChild.children) { + if ((child.isInteraction() || child.isInteractionNode) && child.getValue() === selectedCell.getValue()) { + cells.push(child); + } + } + } + } + + this.graph.getModel().beginUpdate(); + try { + let prevURI = selectedCell.value; + // store + if (prevURI != info.getFullURI()) { + // check for duplication and error + let conflictInteraction = this.getFromInteractionDict(info.getFullURI()); + if (conflictInteraction) { + this.dialog.open(ErrorComponent, { data: "The part " + info.getFullURI() + " already exists as an Interaction!" }); + } + + if (this.getFromInteractionDict(prevURI)) + this.removeFromInteractionDict(prevURI); + this.addToInteractionDict(info); + + for (let cell of cells) { + this.graph.getModel().setValue(cell, info.getFullURI()); + } + } else { + this.updateInteractionDict(info); + } + + + // mutate the cells + for (let cell of cells) { + if (cell.isInteraction()) { + this.mutateInteractionGlyph(info.interactionType, cell); + } else if (cell.isInteractionNode()) { + this.mutateInteractionNodeGlyph(info.interactionType, cell); + } + } + + } finally { + this.graph.getModel().endUpdate(); + } + } + protected isDuplicateURI(newURI: string, ignoreComponents: boolean = false): boolean { let conflictInfo = this.getFromInfoDict(newURI); if (conflictInfo && conflictInfo instanceof ModuleInfo) { @@ -765,20 +827,33 @@ export class GraphHelpers extends GraphBase { continue; let interactions = this.graph.getModel().getChildEdges(viewCell); for (let interaction of interactions) { + 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) { - this.graph.getModel().remove(interaction); + 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){ + delete interactionInfo.toURI[interaction.getId()]; + this.updateInteractionDict(interactionInfo); + this.graph.getModel().remove(interaction); + } continue; } - if (interaction.value.fromURI === oldReference) { - let infoCopy = interaction.value.makeCopy(); - infoCopy.fromURI = newReference; - this.graph.getModel().execute(new GraphEdits.interactionEdit(interaction, infoCopy)); + // replace any interaction references that reference the oldReference + for(let key in interactionInfo.fromURI){ + if(interactionInfo.fromURI[key] == oldReference){ + interactionInfo.fromURI[key] = newReference; + } } - if (interaction.value.toURI === oldReference) { - let infoCopy = interaction.value.makeCopy(); - infoCopy.toURI = newReference; - this.graph.getModel().execute(new GraphEdits.interactionEdit(interaction, infoCopy)); + for(let key in interactionInfo.toURI){ + if(interactionInfo.toURI[key] == oldReference){ + interactionInfo.toURI[key] = newReference; + } } + this.updateInteractionDict(interactionInfo); } } } finally { @@ -789,9 +864,9 @@ export class GraphHelpers extends GraphBase { /** * */ - protected async updateCombinatorialTemplate(oldTemplate: string, newTemplate: string){ + protected async updateCombinatorialTemplate(oldTemplate: string, newTemplate: string) { let combinatorial = this.getCombinatorialWithTemplate(oldTemplate); - if(combinatorial){ + if (combinatorial) { this.removeFromCombinatorialDict(combinatorial.getFullURI()); combinatorial.templateURI = newTemplate; this.addToCombinatorialDict(combinatorial); @@ -870,40 +945,59 @@ export class GraphHelpers extends GraphBase { * one selected in the info menu * @param name */ - protected mutateInteractionGlyph(name: string) { - const selectionCells = this.graph.getSelectionCells(); + protected mutateInteractionGlyph(name: string, cell: mxCell) { + this.graph.getModel().beginUpdate(); + try { - if (selectionCells.length == 1 && selectionCells[0].isInteraction()) { - let selectedCell = selectionCells[0]; + if (name == "Biochemical Reaction" || name == "Non-Covalent Binding" || name == "Genetic Production" || name == "Dissociation") { + name = "Process"; + } + name = GraphBase.STYLE_INTERACTION + name; - this.graph.getModel().beginUpdate(); - try { + // Modify the style string + let styleString = cell.style.slice(); + if (!styleString.includes(';')) { + // nothing special needed, the original style only had the glyphStyleName + styleString = name; + } else { + // the string is something like "strokecolor=#000000;interactionStyleName;fillcolor=#ffffff;etc;etc;" + // we only want to replace the 'glyphStyleName' bit + let startIdx = styleString.indexOf(GraphBase.STYLE_INTERACTION); + let endIdx = styleString.indexOf(';', startIdx); + let stringToReplace = styleString.slice(startIdx, endIdx - startIdx); + styleString = styleString.replace(stringToReplace, name); + } - if (name == "Biochemical Reaction" || name == "Non-Covalent Binding" || name == "Genetic Production") { - name = "Process"; - } - name = GraphBase.STYLE_INTERACTION + name; + console.debug("changing interaction style to: " + styleString); - // Modify the style string - let styleString = selectedCell.style.slice(); - if (!styleString.includes(';')) { - // nothing special needed, the original style only had the glyphStyleName - styleString = name; - } else { - // the string is something like "strokecolor=#000000;interactionStyleName;fillcolor=#ffffff;etc;etc;" - // we only want to replace the 'glyphStyleName' bit - let startIdx = styleString.indexOf(GraphBase.STYLE_INTERACTION); - let endIdx = styleString.indexOf(';', startIdx); - let stringToReplace = styleString.slice(startIdx, endIdx - startIdx); - styleString = styleString.replace(stringToReplace, name); - } + this.graph.getModel().setStyle(cell, styleString); + } finally { + this.graph.getModel().endUpdate(); + } + } - console.debug("changing interaction style to: " + styleString); + protected mutateInteractionNodeGlyph(name: string, cell: mxCell) { + this.graph.getModel().beginUpdate(); + try { + name = GraphBase.STYLE_INTERACTION_NODE + this.interactionNodeTypeToName(name); - this.graph.getModel().setStyle(selectedCell, styleString); - } finally { - this.graph.getModel().endUpdate(); + // Modify the style string + let styleString = cell.style.slice(); + if (!styleString.includes(';')) { + // nothing special needed, the original style only had the interactionNode style name + styleString = name; + } else { + // the string is something like "strokecolor=#000000;interactionNodeStyleName;fillcolor=#ffffff;etc;etc;" + // we only want to replace the 'glyphStyleName' bit + let startIdx = styleString.indexOf(GraphBase.STYLE_INTERACTION_NODE); + let endIdx = styleString.indexOf(';', startIdx); + let stringToReplace = styleString.slice(startIdx, endIdx - startIdx); + styleString = styleString.replace(stringToReplace, name); } + + this.graph.getModel().setStyle(cell, styleString); + } finally { + this.graph.getModel().endUpdate(); } } @@ -1072,8 +1166,8 @@ export class GraphHelpers extends GraphBase { this.metadataService.setSelectedGlyphInfo(glyphInfo.makeCopy()); } } - else if (cell.isInteraction()) { - let interactionInfo = cell.value; + else if (cell.isInteraction() || cell.isInteractionNode()) { + let interactionInfo = this.getFromInteractionDict(cell.value); if (interactionInfo) { this.metadataService.setSelectedInteractionInfo(interactionInfo.makeCopy()); } @@ -1283,6 +1377,46 @@ export class GraphHelpers extends GraphBase { } } + protected trimUnreferencedInfos() { + const cell0 = this.graph.getModel().getCell("0"); + // accumulate all the references + let foundInfos = {}; + let foundInteractions = {}; + for (let cellKey in this.graph.getModel().cells) { + const cell = this.graph.getModel().cells[cellKey]; + if (cell.isCircuitContainer() || cell.isSequenceFeatureGlyph() || cell.isModule() || cell.isMolecularSpeciesGlyph()) { + foundInfos[cell.value] = ""; // the value doesn't matter, as we just want to keep track of the key + } + if (cell.isViewCell()) { + foundInfos[cell.getId()] = ""; + } + if (cell.isInteractionNode() || cell.isInteraction()) { + foundInteractions[cell.value] = ""; + } + } + // detect missing references + let infosToRemove = []; + for (let dictKey in cell0.value[GraphBase.INFO_DICT_INDEX]) { + if (!(dictKey in foundInfos)) { + infosToRemove.push(dictKey); + } + } + let interactionsToRemove = []; + for (let dictKey in cell0.value[GraphBase.INTERACTION_DICT_INDEX]) { + if (!(dictKey in foundInteractions)) { + interactionsToRemove.push(dictKey); + } + } + // remove references + for (let infoURI of infosToRemove) { + this.removeFromInfoDict(infoURI); + } + for (let interactionURI of interactionsToRemove) { + this.removeFromInteractionDict(interactionURI); + } + + } + protected getCoupledGlyphs(glyphURI: string): mxCell[] { const coupledCells = []; for (let key in this.graph.getModel().cells) { @@ -1695,6 +1829,44 @@ export class GraphHelpers extends GraphBase { return false; } + 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); + // fix the geometry if either was null + let sourcePoint = cell.geometry.getTerminalPoint(true); + let targetPoint = cell.geometry.getTerminalPoint(false); + cell.geometry.setTerminalPoint(null, true); + cell.geometry.setTerminalPoint(null, false); + if(sourcePoint){ + cell.geometry.setTerminalPoint(sourcePoint, false); + } + if(targetPoint){ + cell.geometry.setTerminalPoint(targetPoint, true); + } + // reverse the info to/from + let newInfo = this.getFromInteractionDict(cell.value).makeCopy(); + let oldTo = newInfo.toURI[cell.id]; + let oldFrom = newInfo.toURI[cell.id]; + delete newInfo.toURI[cell.id]; + delete newInfo.fromURI[cell.id]; + if(oldTo){ + newInfo.fromURI[cell.id] = oldTo; + } + if(oldFrom){ + newInfo.toURI[cell.id] = oldFrom; + } + // nuke the refinemnets, as source refinements don't match target refinements + delete newInfo.sourceRefinement[cell.id]; + delete newInfo.targetRefinement[cell.id]; + this.updateInteractionDict(newInfo); + this.updateAngularMetadata(this.graph.getSelectionCells()); + } + /** * Updates an Info object * NOTE: Should only be used if the glyphURI is the same @@ -1774,6 +1946,26 @@ export class GraphHelpers extends GraphBase { } } + protected updateInteractionDict(info: InteractionInfo) { + const cell0 = this.graph.getModel().getCell(0); + this.graph.getModel().execute(new GraphEdits.infoEdit(cell0, info, cell0.value[GraphBase.INTERACTION_DICT_INDEX][info.getFullURI()], GraphBase.INTERACTION_DICT_INDEX)); + } + + protected removeFromInteractionDict(interactionURI: string) { + const cell0 = this.graph.getModel().getCell(0); + this.graph.getModel().execute(new GraphEdits.infoEdit(cell0, null, cell0.value[GraphBase.INTERACTION_DICT_INDEX][interactionURI], GraphBase.INTERACTION_DICT_INDEX)); + } + + protected addToInteractionDict(info: InteractionInfo) { + const cell0 = this.graph.getModel().getCell(0); + this.graph.getModel().execute(new GraphEdits.infoEdit(cell0, info, null, GraphBase.INTERACTION_DICT_INDEX)); + } + + protected getFromInteractionDict(interactionURI: string): InteractionInfo { + const cell0 = this.graph.getModel().getCell(0); + return cell0.value[GraphBase.INTERACTION_DICT_INDEX][interactionURI]; + } + protected initLabelDrawing() { // label drawing let graphService = this; @@ -1796,7 +1988,7 @@ export class GraphHelpers extends GraphBase { } else { return info.displayID; } - } else if (cell.isCircuitContainer()) { + } else if (cell.isCircuitContainer() || cell.isInteraction() || cell.isInteractionNode()) { return null; } else { return cell.value; @@ -1872,4 +2064,8 @@ export class GraphHelpers extends GraphBase { return childViewCell; } + + protected showError(message: string) { + this.dialog.open(ErrorComponent, { data: message }); + } } diff --git a/SBOLCanvasFrontend/src/app/graph.service.ts b/SBOLCanvasFrontend/src/app/graph.service.ts index 51234ed7..b2b9b717 100644 --- a/SBOLCanvasFrontend/src/app/graph.service.ts +++ b/SBOLCanvasFrontend/src/app/graph.service.ts @@ -46,35 +46,80 @@ export class GraphService extends GraphHelpers { } isComposite(sequenceFeature): boolean { - if(!sequenceFeature || !sequenceFeature.isSequenceFeatureGlyph()){ + if (!sequenceFeature || !sequenceFeature.isSequenceFeatureGlyph()) { return false; } return sequenceFeature.getCircuitContainer(this.graph).children.length > 1; } - isVariant(sequenceFeature): boolean{ - if(!sequenceFeature || !sequenceFeature.isSequenceFeatureGlyph()){ + isVariant(sequenceFeature): boolean { + if (!sequenceFeature || !sequenceFeature.isSequenceFeatureGlyph()) { return false; } let combinatorial = this.getCombinatorialWithTemplate(sequenceFeature.getParent().value); - if(!combinatorial) + if (!combinatorial) return false; return combinatorial.getVariableComponentInfo(sequenceFeature.getId()); } + /** + * Given the interaction type, checks the selected cells source and target to see if it's allowed. + * @param interactionType + * @returns + */ + isInteractionTypeAllowed(interactionType: string): boolean { + let selected = this.graph.getSelectionCells(); + if (selected.length > 1 || selected.length == 0 || (!selected[0].isInteraction() && !selected[0].isInteractionNode)) { + return false; + } + + if (selected[0].isInteraction()) { + let result = this.validateInteraction(interactionType, selected[0].source, selected[0].target); + if (result == null || result == '') { + return true; + }else{ + return false; + } + } + + if (selected[0].isInteractionNode()) { + return interactionType == "Biochemical Reaction" || interactionType == "Dissociation" || interactionType == "Genetic Production" || interactionType == "Non-Covalent Binding"; + } + + return false; + } + + isSelectedTargetEdge(): boolean{ + let selected = this.graph.getSelectionCells(); + if(selected.lengh > 1 || selected.length == 0 || !selected[0].isInteraction()){ + return false; + } + + return selected[0].target == null || !selected[0].target.isInteractionNode(); + } + + isSelectedSourceEdge(): boolean{ + let selected = this.graph.getSelectionCells(); + if(selected.lengh > 1 || selected.length == 0 || !selected[0].isInteraction()){ + return false; + } + + return selected[0].source == null || !selected[0].source.isInteractionNode(); + } + /** * Recursively checks that all leaf children have sequences * @param sequenceFeature A cell representing a sequence feature */ hasSequence(sequenceFeature): boolean { - if(!sequenceFeature || !sequenceFeature.isSequenceFeatureGlyph()){ + if (!sequenceFeature || !sequenceFeature.isSequenceFeatureGlyph()) { return false; } // check if the child view has more than just a backbone let circuitContainer = sequenceFeature.getCircuitContainer(this.graph); - if(circuitContainer.children.length > 1){ - for(let child of circuitContainer.children){ - if(child.isSequenceFeatureGlyph() && !this.hasSequence(child)){ + if (circuitContainer.children.length > 1) { + for (let child of circuitContainer.children) { + if (child.isSequenceFeatureGlyph() && !this.hasSequence(child)) { return false; } } @@ -82,7 +127,7 @@ export class GraphService extends GraphHelpers { } // no children? we must be a leaf node, check for a sequence let glyphInfo = (this.getFromInfoDict(sequenceFeature.getValue())); - if(!glyphInfo || !glyphInfo.sequence || glyphInfo.sequence.length <= 0){ + if (!glyphInfo || !glyphInfo.sequence || glyphInfo.sequence.length <= 0) { return false; } return true; @@ -91,13 +136,13 @@ export class GraphService extends GraphHelpers { /** * Forces the graph to redraw */ - repaint(){ + repaint() { this.graph.refresh(); } getSelectedCellID(): string { let selected = this.graph.getSelectionCells(); - if(selected.length != 1){ + if (selected.length != 1) { return null; } return selected[0].getId(); @@ -206,15 +251,12 @@ export class GraphService extends GraphHelpers { console.debug("turning east"); } } else if (cell.isInteraction()) { - const src = cell.source; - const dest = cell.target; - let newEdge = this.graph.addEdge(cell, null, dest, src); - // reverse the info to/from - let newInfo = newEdge.value.makeCopy(); - let tmpTo = newInfo.to; - newInfo.to = newInfo.from; - newInfo.from = tmpTo; - this.graph.getModel().execute(new GraphEdits.interactionEdit(newEdge, newInfo)); + this.flipInteractionEdge(cell); + }else if (cell.isInteractionNode()){ + let edges = this.graph.getModel().getEdges(cell); + for(let edge of edges){ + this.flipInteractionEdge(edge); + } } } @@ -365,14 +407,14 @@ export class GraphService extends GraphHelpers { try { let circuitContainers = []; for (let cell of selectedCells) { - if (cell.isSequenceFeatureGlyph()){ + 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()){ + if (cell.isSequenceFeatureGlyph()) { let combinatorial = this.getCombinatorialWithTemplate(cell.getParent().getValue()); // TODO make this undoable - if(combinatorial) + if (combinatorial) combinatorial.removeVariableComponentInfo(cell.getId()); } } else if (cell.isCircuitContainer() && this.graph.getCurrentRoot() && this.graph.getCurrentRoot().isComponentView()) @@ -410,7 +452,7 @@ 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) { @@ -423,6 +465,7 @@ export class GraphService extends GraphHelpers { this.trimUnreferencedCells(); this.trimUnreferencedCombinatorials(); + this.trimUnreferencedInfos(); // sync circuit containers for (let circuitContainer of circuitContainers) { @@ -714,6 +757,36 @@ export class GraphService extends GraphHelpers { console.log(this.graph.getModel().cells); } + makeInteractionNodeDragsource(element, stylename) { + const insertGlyph = mx.mxUtils.bind(this, function (graph, evt, target, x, y) { + this.addInteractionNodeAt(stylename, x - GraphBase.interactionNodeGlyphWidth / 2, y - GraphBase.interactionNodeGlyphHeight / 2); + }); + this.makeGeneralDragsource(element, insertGlyph); + } + + addInteractionNode(name) { + const pt = this.getDefaultNewCellCoords(); + this.addInteractionNodeAt(name, pt.x, pt.y); + } + + addInteractionNodeAt(name: string, x, y) { + this.graph.getModel().beginUpdate(); + try { + let interactionInfo = new InteractionInfo(); + const interactionNodeGlyph = this.graph.insertVertex(this.graph.getDefaultParent(), null, interactionInfo.getFullURI(), x, y, + GraphBase.interactionNodeGlyphWidth, GraphBase.interactionNodeGlyphHeight, GraphBase.STYLE_INTERACTION_NODE + name); + interactionInfo.interactionType = this.interactionNodeNametoType(name); + this.addToInteractionDict(interactionInfo); + interactionNodeGlyph.setConnectable(true); + + // The new glyph should be selected + this.graph.clearSelection(); + this.graph.setSelectionCell(interactionNodeGlyph); + } finally { + this.graph.getModel().endUpdate(); + } + } + /** * Turns the given HTML element into a dragsource for creating interaction glyphs */ @@ -756,37 +829,80 @@ export class GraphService extends GraphHelpers { this.graph.getModel().beginUpdate(); try { - cell = new mx.mxCell(new InteractionInfo(), new mx.mxGeometry(x, y, 0, 0), GraphBase.STYLE_INTERACTION + name); + let addToDictionary = true; + let interactionInfo = new InteractionInfo(); + cell = new mx.mxCell(interactionInfo.getFullURI(), new mx.mxGeometry(x, y, 0, 0), GraphBase.STYLE_INTERACTION + name); const selectionCells = this.graph.getSelectionCells(); if (selectionCells.length == 1) { + + // one cell is selected, set the edges source const selectedCell = this.graph.getSelectionCell(); + // check for any restrictions on valid edges + let error = this.graph.getEdgeValidationError(cell, selectedCell, null); + if (error) { + this.showError(error); + return; + } if (selectedCell.isModule()) { + // if the cell is a module, we need to prompt what subpart we want to connect to let result = await this.promptChooseFunctionalComponent(selectedCell, true); if (!result) return; - cell.value.fromURI = result; + interactionInfo.fromURI[this.graph.getModel().nextId] = result; + } else if (selectedCell.isInteractionNode()) { + // if the source is a interaction node, we want to inherit it's information + cell.value = selectedCell.value; + addToDictionary = false; + interactionInfo = this.getFromInteractionDict(selectedCell.value).makeCopy() } cell.geometry.setTerminalPoint(new mx.mxPoint(x, y - GraphBase.defaultInteractionSize), false); cell.edge = true; this.graph.addEdge(cell, this.graph.getCurrentRoot(), selectedCell, null); + } else if (selectionCells.length == 2) { + + // two cells were selected, set the first one as the source, and the second as the target const sourceCell = selectionCells[0]; const destCell = selectionCells[1]; + // check for restrictions on the edge + let error = this.graph.getEdgeValidationError(cell, sourceCell, destCell); + if (error) { + this.showError(error); + return; + } + // check source or target are interaction nodes to couple with them before making modifications to the interaction + // don't worry, edge validation rules prevent both from being interaction nodes. + if(sourceCell.isInteractionNode()){ + // inherit the information + cell.value = sourceCell.value; + addToDictionary = false; + interactionInfo = this.getFromInteractionDict(sourceCell.value).makeCopy(); + } + if(destCell.isInteractionNode()){ + // inherit the information + cell.value = destCell.value; + addToDictionary = false; + interactionInfo = this.getFromInteractionDict(destCell.value).makeCopy(); + } + if (sourceCell.isModule()) { + // prompt for the subpart to keep track of let result = await this.promptChooseFunctionalComponent(sourceCell, true); if (!result) return; - cell.value.fromURI = result; + interactionInfo.fromURI[this.graph.getModel().nextId] = result; } if (destCell.isModule()) { + // prompt for the subpart to keep track of let result = await this.promptChooseFunctionalComponent(destCell, false); if (!result) return; - cell.value.toURI = result; + interactionInfo.toURI[this.graph.getModel().nextId] = result; } cell.edge = true; this.graph.addEdge(cell, this.graph.getCurrentRoot(), sourceCell, destCell); + } else { cell.geometry.setTerminalPoint(new mx.mxPoint(x, y + GraphBase.defaultInteractionSize), true); cell.geometry.setTerminalPoint(new mx.mxPoint(x + GraphBase.defaultInteractionSize, y), false); @@ -799,8 +915,14 @@ export class GraphService extends GraphHelpers { if (name == "Process") { name = "Genetic Production" } - //cell.data = new InteractionInfo(); - cell.value.interactionType = name; + + if (addToDictionary) { + interactionInfo.interactionType = name; + this.addToInteractionDict(interactionInfo); + } else { + this.updateInteractionDict(interactionInfo); + this.mutateInteractionGlyph(interactionInfo.interactionType, cell); + } // The new glyph should be selected this.graph.clearSelection(); @@ -901,11 +1023,8 @@ export class GraphService extends GraphHelpers { return; } - if (info instanceof InteractionInfo && selectedCell.isInteraction()) { - let interactionEdit = new GraphEdits.interactionEdit(selectedCell, info); - this.mutateInteractionGlyph(info.interactionType); - this.graph.getModel().execute(interactionEdit); - return; + if (info instanceof InteractionInfo && (selectedCell.isInteraction() || selectedCell.isInteractionNode())) { + this.updateSelectedInteractionInfo(info); } } finally { @@ -926,7 +1045,7 @@ export class GraphService extends GraphHelpers { this.graph.getModel().beginUpdate(); try { if (info instanceof CombinatorialInfo && selectedCell.isSequenceFeatureGlyph()) { - if(!prevURI){ + if (!prevURI) { prevURI = info.getFullURI(); } this.updateSelectedCombinatorialInfo(info, prevURI); @@ -1110,6 +1229,8 @@ export class GraphService extends GraphHelpers { this.trimUnreferencedCells(); this.editor.undoManager.clear(); + + this.graph.refresh(); // for some reason unformatted edges don't render correctly the first time without this } /** @@ -1257,6 +1378,115 @@ export class GraphService extends GraphHelpers { this.removeFromInfoDict(viewCells[i].getId()); } this.addToInfoDict(subGlyphDict[GraphBase.INFO_DICT_INDEX][viewCells[i].getId()]); + + // add any molecular species or interactions to the info dict + for(let child of viewClone.children){ + if(child.isMolecularSpeciesGlyph()){ + if(this.getFromInfoDict(child.value) != null){ + this.removeFromInfoDict(child.value); + } + this.addToInfoDict(subGlyphDict[GraphBase.INFO_DICT_INDEX][child.value]); + }else if(child.isInteractionNode()){ + if(this.getFromInteractionDict(child.value) != null){ + this.removeFromInteractionDict(child.value); + } + this.addToInteractionDict(subGlyphDict[GraphBase.INTERACTION_DICT_INDEX][child.value]); + }else if(child.isInteraction()){ + // if either end is an interaction node, we don't need to bother + if((child.source && child.source.isInteractionNode()) || (child.target && child.target.isInteractionNode())){ + continue; + } + if(this.getFromInteractionDict(child.value) != null){ + this.removeFromInteractionDict(child.value); + } + this.addToInteractionDict(subGlyphDict[GraphBase.INTERACTION_DICT_INDEX][child.value]); + } + } + } + + // relink the interactions now that their ID's have likely changed + for(let i = 1; i < viewCells.length; i++){ + let viewClone = this.graph.getModel().getCell(viewCells[i].getId()); + for(let j = 0; j < viewClone.children.length; j++){ + // for now it seems that cloning the cell keeps the child order in tact + let child = viewClone.children[j]; + let originalChild = viewCells[i].children[j]; + if(child.isInteractionNode()){ + // copy to new dict as new id's may conflict with old + let newTo = [] + let newFrom = [] + let newSource = [] + let newTarget = [] + let infoCopy = this.getFromInteractionDict(child.value).makeCopy(); + for(let k = 0; k < child.edges.length; k++){ + let edge = child.edges[k]; + let originalEdge = originalChild.edges[k]; + if(infoCopy.toURI[originalEdge.getId()]){ + // find the original cell + let cellRef = infoCopy.toURI[originalEdge.getId()]; + let oldCell = subGraph.getModel().getCell(cellRef.substring(cellRef.lastIndexOf("_")+1)); + let oldParentView = oldCell.getParent(); + let newParentView = this.graph.getModel().getCell(oldParentView.getId()); + // replace with the new id + newTo[edge.getId()] = cellRef.substring(0,cellRef.lastIndexOf("_")+1)+newParentView.children[oldParentView.getIndex(oldCell)].getId(); + } + if(infoCopy.fromURI[originalEdge.getId()]){ + // find the original cell + let cellRef = infoCopy.fromURI[originalEdge.getId()]; + let oldCell = subGraph.getModel().getCell(cellRef.substring(cellRef.lastIndexOf("_")+1)); + let oldParentView = oldCell.getParent(); + let newParentView = this.graph.getModel().getCell(oldParentView.getId()); + // replace with the new id + newFrom[edge.getId()] = cellRef.substring(0,cellRef.lastIndexOf("_")+1)+newParentView.children[oldParentView.getIndex(oldCell)].getId(); + } + if(infoCopy.sourceRefinement[originalEdge.getId()]){ + newSource[edge.getId()] = infoCopy.sourceRefinement[originalEdge.getId()]; + } + if(infoCopy.targetRefinement[originalEdge.getId()]){ + newTarget[edge.getId()] = infoCopy.targetRefinement[originalEdge.getId()]; + } + } + infoCopy.toURI = newTo; + infoCopy.fromURI = newFrom; + infoCopy.sourceRefinement = newSource; + infoCopy.targetRefinement = newTarget; + this.updateInteractionDict(infoCopy); + } + if(child.isInteraction()){ + // skip edges connected to interaction nodes + if((child.source && child.source.isInteractionNode()) || (child.target && child.target.isInteractionNode())){ + continue; + } + let infoCopy = this.getFromInteractionDict(child.value).makeCopy(); + if(infoCopy.fromURI[originalChild.getId()]){ + let cellRef = infoCopy.fromURI[originalChild.getId()]; + delete infoCopy.fromURI[originalChild.getId()]; + let oldCell = subGraph.getModel().getCell(cellRef.substring(cellRef.lastIndexOf("_")+1)); + let oldParentView = oldCell.getParent(); + let newParentView = this.graph.getModel().getCell(oldParentView.getId()); + infoCopy.fromURI[child.getId()] = cellRef.substring(0,cellRef.lastIndexOf("_")+1)+newParentView.children[oldParentView.getIndex(oldCell)].getId();; + } + if(infoCopy.toURI[originalChild.getId()]){ + let cellRef = infoCopy.toURI[originalChild.getId()]; + delete infoCopy.toURI[originalChild.getId()]; + let oldCell = subGraph.getModel().getCell(cellRef.substring(cellRef.lastIndexOf("_")+1)); + let oldParentView = oldCell.getParent(); + let newParentView = this.graph.getModel().getCell(oldParentView.getId()); + infoCopy.toURI[child.getId()] = cellRef.substring(0,cellRef.lastIndexOf("_")+1)+newParentView.children[oldParentView.getIndex(oldCell)].getId();; + } + if(infoCopy.sourceRefinement[originalChild.getId()]){ + let value = infoCopy.sourceRefinement[originalChild.getId()]; + delete infoCopy.sourceRefinement[originalChild.getId()]; + infoCopy.sourceRefinement[child.getId()] = value; + } + if(infoCopy.targetRefinement[originalChild.getId()]){ + let value = infoCopy.targetRefinement[originalChild.getId()]; + delete infoCopy.targetRefinement[originalChild.getId()]; + infoCopy.targetRefinement[child.getId()] = value; + } + this.updateInteractionDict(infoCopy); + } + } } if (selectedCell.isSequenceFeatureGlyph()) { diff --git a/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.html b/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.html index b00867db..e9e6033a 100644 --- a/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.html +++ b/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.html @@ -96,12 +96,34 @@ Interaction Type - + {{type}} + Source Role: {{getSourceInteractionRole()}} + + + Source Refinement + + + {{refinement}} + + + + + Target Role: {{getTargetInteractionRole()}} + + + Target Refinement + + + {{refinement}} + + + + diff --git a/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts b/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts index cc1a585a..e57741fc 100644 --- a/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts +++ b/SBOLCanvasFrontend/src/app/info-editor/info-editor.component.ts @@ -9,6 +9,7 @@ import { DownloadGraphComponent } from '../download-graph/download-graph.compone import { ModuleInfo } from '../moduleInfo'; import { environment } from 'src/environments/environment'; import { CombinatorialDesignEditorComponent } from '../combinatorial-design-editor/combinatorial-design-editor.component'; +import { ThrowStmt } from '@angular/compiler'; @Component({ @@ -26,6 +27,10 @@ export class InfoEditorComponent implements OnInit { partRoles: string[]; partRefinements: string[]; // these depend on role interactionTypes: string[]; + filteredInteractionTypes: string[]; + interactionRoles: {}; + interactionSourceRefinements: String[]; + interactionTargetRefinements: String[]; // TODO get these from the backend encodings: string[]; @@ -44,6 +49,7 @@ export class InfoEditorComponent implements OnInit { this.getTypes(); this.getRoles(); this.getInteractions(); + this.getInteractionRoles(); } getTypes() { @@ -62,6 +68,18 @@ export class InfoEditorComponent implements OnInit { this.metadataService.loadInteractions().subscribe(interactions => this.interactionTypes = interactions); } + getInteractionRoles() { + this.metadataService.loadInteractionRoles().subscribe(interactionRoles => this.interactionRoles = interactionRoles); + } + + getInteractionSourceRefinements(sourceRole: string) { + this.metadataService.loadInteractionRoleRefinements(sourceRole).subscribe(sourceRefinements => this.interactionSourceRefinements = sourceRefinements); + } + + getInteractionTargetRefinements(targetRole: string) { + this.metadataService.loadInteractionRoleRefinements(targetRole).subscribe(targetRefinements => this.interactionTargetRefinements = targetRefinements); + } + dropDownChange(event: MatSelectChange) { const id = event.source.id; @@ -88,6 +106,16 @@ export class InfoEditorComponent implements OnInit { } case 'interactionType': { this.interactionInfo.interactionType = event.value; + this.getInteractionSourceRefinements(event.value); + this.getInteractionTargetRefinements(event.value); + break; + } + case 'interactionSourceRefinement': { + this.interactionInfo.sourceRefinement[this.graphService.getSelectedCellID()] = event.value; + break; + } + case 'interactionTargetRefinement': { + this.interactionInfo.targetRefinement[this.graphService.getSelectedCellID()] = event.value; break; } default: { console.log('Unexpected id encountered in info menu dropdown = ' + id); @@ -112,29 +140,29 @@ export class InfoEditorComponent implements OnInit { this.glyphInfo.displayID = replaced; } else if (this.interactionInfo != null) { this.interactionInfo.displayID = replaced; - } else if (this.moduleInfo){ + } else if (this.moduleInfo) { this.moduleInfo.displayID = replaced; } break; } case 'name': { - if(this.glyphInfo) + if (this.glyphInfo) this.glyphInfo.name = event.target.value; - else if(this.moduleInfo) + else if (this.moduleInfo) this.moduleInfo.name = event.target.value; break; } case 'description': { - if(this.glyphInfo) + if (this.glyphInfo) this.glyphInfo.description = event.target.value; - else if(this.moduleInfo) + else if (this.moduleInfo) this.moduleInfo.description = event.target.value; break; } case 'version': { - if(this.glyphInfo) + if (this.glyphInfo) this.glyphInfo.version = event.target.value; - else if(this.moduleInfo) + else if (this.moduleInfo) this.moduleInfo.version = event.target.value; break; } @@ -151,7 +179,7 @@ export class InfoEditorComponent implements OnInit { this.graphService.setSelectedCellInfo(this.glyphInfo); } else if (this.interactionInfo != null) { this.graphService.setSelectedCellInfo(this.interactionInfo); - } else if (this.moduleInfo != null){ + } else if (this.moduleInfo != null) { this.graphService.setSelectedCellInfo(this.moduleInfo); } } @@ -171,8 +199,8 @@ export class InfoEditorComponent implements OnInit { return this.graphService.isSelectedAGlyph() && this.graphService.isRootAComponentView(); } - openCombinatorialDialog(){ - this.dialog.open(CombinatorialDesignEditorComponent).afterClosed().subscribe( _ => { + openCombinatorialDialog() { + this.dialog.open(CombinatorialDesignEditorComponent).afterClosed().subscribe(_ => { this.graphService.repaint(); }); } @@ -198,7 +226,7 @@ export class InfoEditorComponent implements OnInit { /** * Updates both the module info in the form and in the graph. */ - moduleInfoUpdated(moduleInfo: ModuleInfo){ + moduleInfoUpdated(moduleInfo: ModuleInfo) { this.moduleInfo = moduleInfo; // this needs to be called because we may have gotten here from an async function // an async function doesn't update the view for some reason @@ -210,24 +238,42 @@ export class InfoEditorComponent implements OnInit { */ interactionInfoUpdated(interactionInfo: InteractionInfo) { this.interactionInfo = interactionInfo; + if (interactionInfo != null) { + if (interactionInfo.interactionType != null) { + this.getInteractionSourceRefinements(this.interactionRoles[interactionInfo.interactionType][0]); + this.getInteractionTargetRefinements(this.interactionRoles[interactionInfo.interactionType][1]); + } else { + this.interactionSourceRefinements = []; + this.interactionTargetRefinements = []; + } + + // filter valid interaction types + this.filteredInteractionTypes = []; + for (let type of this.interactionTypes) { + if (this.graphService.isInteractionTypeAllowed(type)) { + this.filteredInteractionTypes.push(type); + } + } + } + // this needs to be called because we may have gotten here from an async function // an async function doesn't update the view for some reason this.changeDetector.detectChanges(); } localDesign(): boolean { - if(this.glyphInfo) + if (this.glyphInfo) return this.glyphInfo.uriPrefix === environment.baseURI; - else if(this.moduleInfo) + else if (this.moduleInfo) return this.moduleInfo.uriPrefix === environment.baseURI; return true; } synBioHubDesign(): boolean { for (let registry of this.registries) { - if(this.glyphInfo && this.glyphInfo.uriPrefix && this.glyphInfo.uriPrefix.startsWith(registry)) + if (this.glyphInfo && this.glyphInfo.uriPrefix && this.glyphInfo.uriPrefix.startsWith(registry)) return true; - if(this.moduleInfo && this.moduleInfo.uriPrefix && this.moduleInfo.uriPrefix.startsWith(registry)) + if (this.moduleInfo && this.moduleInfo.uriPrefix && this.moduleInfo.uriPrefix.startsWith(registry)) return true; } return false; @@ -237,4 +283,22 @@ export class InfoEditorComponent implements OnInit { return !this.localDesign() && !this.synBioHubDesign(); } + getSourceInteractionRole() { + let sourceRole = this.interactionRoles[this.interactionInfo.interactionType][0]; + return sourceRole ? sourceRole : "NA"; + } + + getTargetInteractionRole() { + let targetRole = this.interactionRoles[this.interactionInfo.interactionType][1]; + return targetRole ? targetRole : "NA"; + } + + hasSourceRefinements(): boolean { + return this.interactionSourceRefinements && this.interactionSourceRefinements.length > 0; + } + + hasTargetRefinements(): boolean { + return this.interactionTargetRefinements && this.interactionTargetRefinements.length > 0; + } + } diff --git a/SBOLCanvasFrontend/src/app/interactionInfo.ts b/SBOLCanvasFrontend/src/app/interactionInfo.ts index 1203c6ba..56aa4b38 100644 --- a/SBOLCanvasFrontend/src/app/interactionInfo.ts +++ b/SBOLCanvasFrontend/src/app/interactionInfo.ts @@ -5,33 +5,58 @@ export class InteractionInfo extends Info { // Remember that when you change this you need to change the encode function in graph service static counter: number = 0; interactionType: string; - fromURI: string; - toURI: string; + + // treat these as dictionaries, not arrays + sourceRefinement = []; + targetRefinement = []; + fromURI = []; + toURI = []; constructor() { super(); this.displayID = 'Interaction' + (InteractionInfo.counter++); - this.interactionType = "Yo momma"; } makeCopy() { const copy: InteractionInfo = new InteractionInfo(); copy.displayID = this.displayID; + copy.uriPrefix = this.uriPrefix; copy.interactionType = this.interactionType; - copy.fromURI = this.fromURI; - copy.toURI = this.toURI; + for(let key in this.sourceRefinement){ + copy.sourceRefinement[key] = this.sourceRefinement[key]; + } + for(let key in this.targetRefinement){ + copy.targetRefinement[key] = this.targetRefinement[key]; + } + for(let key in this.fromURI){ + copy.fromURI[key] = this.fromURI[key]; + } + for(let key in this.toURI){ + copy.toURI[key] = this.toURI[key]; + } return copy; } copyDataFrom(other: InteractionInfo) { this.displayID = other.displayID; + this.uriPrefix = other.uriPrefix; this.interactionType = other.interactionType; - this.fromURI = other.fromURI; - this.toURI = other.toURI; + for(let key in other.sourceRefinement){ + this.sourceRefinement[key] = other.sourceRefinement[key]; + } + for(let key in other.targetRefinement){ + this.targetRefinement[key] = other.targetRefinement[key]; + } + for(let key in other.fromURI){ + this.fromURI[key] = other.fromURI[key]; + } + for(let key in other.toURI){ + this.toURI[key] = other.toURI[key]; + } } getFullURI() { - return environment.baseURI + '/' + this.displayID; + return this.uriPrefix + '/' + this.displayID; } encode(enc: any) { @@ -40,10 +65,52 @@ export class InteractionInfo extends Info { node.setAttribute("displayID", this.displayID); if (this.interactionType) node.setAttribute("interactionType", this.interactionType); - if(this.fromURI) - node.setAttribute("fromURI", this.fromURI); - if(this.toURI) - node.setAttribute("toURI", this.toURI); + if(this.uriPrefix) + node.setAttribute("uriPrefix", this.uriPrefix); + if(this.sourceRefinement){ + let sourceRefinementNode = enc.document.createElement('Array'); + sourceRefinementNode.setAttribute("as", "sourceRefinement"); + for (let key in this.sourceRefinement) { + let sourceRefNode = enc.document.createElement("add"); + sourceRefNode.setAttribute("value", this.sourceRefinement[key]); + sourceRefNode.setAttribute("as", key); + sourceRefinementNode.appendChild(sourceRefNode); + } + node.appendChild(sourceRefinementNode); + } + if(this.targetRefinement){ + let targetRefinementNode = enc.document.createElement("Array"); + targetRefinementNode.setAttribute("as", "targetRefinement"); + for(let key in this.targetRefinement){ + let targetRefNode = enc.document.createElement("add"); + targetRefNode.setAttribute("value", this.targetRefinement[key]); + targetRefNode.setAttribute("as", key); + targetRefinementNode.appendChild(targetRefNode); + } + node.appendChild(targetRefinementNode); + } + if(this.fromURI){ + let fromURINode = enc.document.createElement("Array"); + fromURINode.setAttribute("as", "fromURI"); + for(let key in this.fromURI){ + let URINode = enc.document.createElement("add"); + URINode.setAttribute("value", this.fromURI[key]); + URINode.setAttribute("as", key); + fromURINode.appendChild(URINode); + } + node.appendChild(fromURINode); + } + if(this.toURI){ + let toURINode = enc.document.createElement("Array"); + toURINode.setAttribute("as", "toURI"); + for(let key in this.toURI){ + let URINode = enc.document.createElement("add"); + URINode.setAttribute("value", this.toURI[key]); + URINode.setAttribute("as", key); + toURINode.appendChild(URINode); + } + node.appendChild(toURINode); + } return node; } } diff --git a/SBOLCanvasFrontend/src/app/metadata.service.ts b/SBOLCanvasFrontend/src/app/metadata.service.ts index 365f71a4..6c4f9c73 100644 --- a/SBOLCanvasFrontend/src/app/metadata.service.ts +++ b/SBOLCanvasFrontend/src/app/metadata.service.ts @@ -37,6 +37,8 @@ export class MetadataService { private rolesURL = environment.backendURL + '/data/roles'; private refinementsURL = environment.backendURL + '/data/refine'; private interactionsURL = environment.backendURL + '/data/interactions'; + private interactionRolesURL = environment.backendURL + '/data/interactionRoles'; + private interactionRoleRefinementURL = environment.backendURL + '/data/interactionRoleRefine'; // Glyph Info private glyphInfoSource = new BehaviorSubject(null); @@ -86,6 +88,16 @@ export class MetadataService { return this.http.get(this.interactionsURL); } + loadInteractionRoles() : Observable { + return this.http.get(this.interactionRolesURL); + } + + loadInteractionRoleRefinements(parent: string): Observable { + let params = new HttpParams(); + params = params.append("parent", parent); + return this.http.get(this.interactionRoleRefinementURL, {params: params}); + } + setSelectedStyleInfo(newInfo: StyleInfo) { this.styleInfoSource.next(newInfo); } diff --git a/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.html b/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.html index 8779b563..ff72c8eb 100644 --- a/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.html +++ b/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.html @@ -143,9 +143,9 @@ [matTooltip]="'Focus out to the parent part'" (click)="graphService.exitGlyph()">open_in_browser - + (click)="testMethod()">TEST --> diff --git a/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.ts b/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.ts index 019572fd..be078b04 100644 --- a/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.ts +++ b/SBOLCanvasFrontend/src/app/toolbar/toolbar.component.ts @@ -119,8 +119,9 @@ export class ToolbarComponent implements OnInit, AfterViewInit { return this.graphService.isRootAComponentView(); } - // make sure to comment out the button when preparing to make a merge request + // make sure to comment out the button in the html when preparing to make a merge request testMethod(){ - this.dialog.open(DownloadGraphComponent); + this.graphService.addMolecularSpecies("replacement-glyph"); + this.graphService.addInteractionNode("replacement-glyph"); } } diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/association.xml b/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/association.xml index 7a46c716..e199b79b 100644 --- a/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/association.xml +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/association.xml @@ -6,8 +6,6 @@ - - @@ -18,6 +16,7 @@ + diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/replacement-glyph.xml b/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/replacement-glyph.xml new file mode 100644 index 00000000..4138164d --- /dev/null +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/interaction_nodes/replacement-glyph.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SBOLCanvasFrontend/src/assets/glyph_stencils/molecular_species/complex.xml b/SBOLCanvasFrontend/src/assets/glyph_stencils/molecular_species/complex.xml new file mode 100644 index 00000000..c1e455d2 --- /dev/null +++ b/SBOLCanvasFrontend/src/assets/glyph_stencils/molecular_species/complex.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/complex.xml b/resources/complex.xml new file mode 100644 index 00000000..a29a4829 --- /dev/null +++ b/resources/complex.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/documents.xml b/resources/documents.xml new file mode 100644 index 00000000..c773903c --- /dev/null +++ b/resources/documents.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file