
org.biopax.paxtools.io.sbgn.L3ToSBGNPDConverter Maven / Gradle / Ivy
package org.biopax.paxtools.io.sbgn;
import org.biopax.paxtools.controller.ModelUtils;
import org.biopax.paxtools.io.sbgn.idmapping.HGNC;
import org.biopax.paxtools.model.BioPAXElement;
import org.biopax.paxtools.model.BioPAXLevel;
import org.biopax.paxtools.model.Model;
import org.biopax.paxtools.model.level3.*;
import org.biopax.paxtools.util.ClassFilterSet;
import org.sbgn.ArcClazz;
import org.sbgn.Language;
import org.sbgn.SbgnUtil;
import org.sbgn.bindings.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//import org.w3c.dom.Document;
//import org.w3c.dom.Element;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
//import javax.xml.parsers.DocumentBuilder;
//import javax.xml.parsers.DocumentBuilderFactory;
//import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.*;
import java.util.Map;
import static org.sbgn.GlyphClazz.*;
import static org.sbgn.ArcClazz.*;
/**
* This class converts BioPAX L3 model into SBGN PD (SBGN-ML XML).
* It optionally applies a special COSE layout.
*
* Currently, this converter ignores several BioPAX types/properties:
*
* - Parent-child relationship between entity references
* - Binding features and covalent binding features of physical entities
* - Pathway, PathwayStep, Evidence, etc.
*
*
* Also note that:
*
* - Compartment is just a controlled vocabulary in BioPAX, so nesting and neighborhood relations
* between compartments are not handled here.
* - Control structures in BioPAX and in SBGN PD are a little different. We use AND and NOT
* glyphs to approximate controls in BioPAX. However, AND-ing everything is not really proper,
* because BioPAX does not imply a logical operator between controllers.
*
*
* @author Ozgun Babur
*/
public class L3ToSBGNPDConverter
{
private static final Logger log = LoggerFactory.getLogger(L3ToSBGNPDConverter.class);
/**
* A matching between physical entities and SBGN classes.
*/
private static Map, String> typeMatchMap;
/**
* For creating SBGN objects.
*/
private static ObjectFactory factory;
//-- Section: Instance variables --------------------------------------------------------------|
/**
* This class is used for detecting ubiques.
*/
protected UbiqueDetector ubiqueDet;
/**
* This class is used for generating short printable strings (text in info boxes) from
* recognized entity features.
*/
protected FeatureDecorator featStrGen;
/**
* Flag to run a layout before writing down the sbgn.(memberPhysicalEntity)
*/
protected boolean doLayout;
/**
* If the number of nodes (biological processes and participants) in the model
* is going to be greater than this maximum, then no layout (but a trivial one)
* will be applied.
*/
protected int maxNodes;
/**
* Mapping from SBGN IDs to the IDs of the related objects in BioPAX.
*/
protected Map> sbgn2BPMap;
/**
* Option to flatten nested complexes.
*/
protected boolean flattenComplexContent;
/**
* SBGN process glyph can be used to show reversible reactions. In that case two ports of the
* process will only have product glyphs. However, this creates an incompatibility with BioPAX:
* reversible biochemical reactions can have catalysis with a direction. But if we use a single
* glyph for the process and direct that catalysis to it, then the direction of the catalysis
* will be lost. If we use two process nodes for the reversible reaction (one for left-to-right
* and another for right-to-left), then we can direct the directed catalysis to only the
* relevant process glyph.
*
* Also note that the layout do not support ports. If layout is run, the ports are removed. In
* that case it will be impossible to distinguish left and right components of the reversible
* process glyph because they will all be product edges.
*/
protected boolean useTwoGlyphsForReversibleConversion;
/**
* ID to glyph map.
*/
Map glyphMap;
/**
* ID to Arc map
*/
Map arcMap;
/**
* ID to compartment map.
*/
Map compartmentMap;
/**
* Set of ubiquitous molecules.
*/
Set ubiqueSet;
// this is to store BioPAX metadata (as XML) in the SBGN-ML Extensions or Notes.
// private static Document biopaxMetaDoc;
// private static final String BIOPAX_NS = "http://www.biopax.org/release/biopax-level3.owl#";
//-- Section: Static initialization -----------------------------------------------------------|
static
{
factory = new ObjectFactory();
typeMatchMap = new HashMap, String>();
typeMatchMap.put(Protein.class, MACROMOLECULE.getClazz());
typeMatchMap.put(SmallMolecule.class, SIMPLE_CHEMICAL.getClazz());
typeMatchMap.put(Dna.class, NUCLEIC_ACID_FEATURE.getClazz());
typeMatchMap.put(Rna.class, NUCLEIC_ACID_FEATURE.getClazz());
typeMatchMap.put(DnaRegion.class, NUCLEIC_ACID_FEATURE.getClazz());
typeMatchMap.put(RnaRegion.class, NUCLEIC_ACID_FEATURE.getClazz());
typeMatchMap.put(NucleicAcid.class, NUCLEIC_ACID_FEATURE.getClazz());
typeMatchMap.put(PhysicalEntity.class, UNSPECIFIED_ENTITY.getClazz());
//TODO: SimplePhysicalEntity is a non-instantiable abstract type in Paxtools; remove the mapping below?
typeMatchMap.put(SimplePhysicalEntity.class, UNSPECIFIED_ENTITY.getClazz());
typeMatchMap.put(Complex.class, COMPLEX.getClazz());
typeMatchMap.put(Gene.class, NUCLEIC_ACID_FEATURE.getClazz());
// //a document for adding metadata elements to insert into SBGN-ML PD glyphs, etc. (inside Extensions/Notes element).
// DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// try {
// DocumentBuilder db = dbf.newDocumentBuilder();
// db.reset();
// biopaxMetaDoc = db.newDocument();
// } catch (ParserConfigurationException e) {
// throw new RuntimeException("Cannot initialize BioPAX extensions DOM.", e);
// }
}
//-- Section: Public methods ------------------------------------------------------------------|
/**
* Constructor.
*/
public L3ToSBGNPDConverter()
{
this(null, null, false);
}
/**
* Constructor with parameters.
* @param ubiqueDet Ubique detector class
* @param featStrGen feature string generator class
* @param doLayout whether we want to perform layout after SBGN creation.
*/
public L3ToSBGNPDConverter(UbiqueDetector ubiqueDet, FeatureDecorator featStrGen,
boolean doLayout)
{
this.ubiqueDet = ubiqueDet;
this.featStrGen = (featStrGen != null) ? featStrGen : new CommonFeatureStringGenerator();
this.doLayout = doLayout;
this.useTwoGlyphsForReversibleConversion = true;
this.sbgn2BPMap = new HashMap>();
this.flattenComplexContent = true;
this.maxNodes = 1000;
}
public void setDoLayout(boolean doLayout) {
this.doLayout = doLayout;
}
/**
* Getter class for the parameter useTwoGlyphsForReversibleConversion.
* @return whether use two glyphs for the reversible conversion
*/
public boolean isUseTwoGlyphsForReversibleConversion()
{
return useTwoGlyphsForReversibleConversion;
}
/**
* Sets the option to use two glyphs for the reversible conversion.
* @param useTwoGlyphsForReversibleConversion give true if use two glyphs
*/
public void setUseTwoGlyphsForReversibleConversion(boolean useTwoGlyphsForReversibleConversion)
{
this.useTwoGlyphsForReversibleConversion = useTwoGlyphsForReversibleConversion;
}
public boolean isFlattenComplexContent()
{
return flattenComplexContent;
}
public void setFlattenComplexContent(boolean flattenComplexContent)
{
this.flattenComplexContent = flattenComplexContent;
}
/**
* Converts the given model to SBGN, and writes in the specified file.
*
* @param model model to convert
* @param file file to write
*/
public void writeSBGN(Model model, String file)
{
// Create the model
Sbgn sbgn = createSBGN(model);
// Write in file
try {
SbgnUtil.writeToFile(sbgn, new File(file));
}
catch (JAXBException e) {
throw new RuntimeException("writeSBGN, SbgnUtil.writeToFile failed", e);
}
}
/**
* Converts the given model to SBGN, and writes in the specified output stream.
*
* @param model model to convert
* @param stream output stream to write
*/
public void writeSBGN(Model model, OutputStream stream)
{
Sbgn sbgn = createSBGN(model);
try {
JAXBContext context = JAXBContext.newInstance("org.sbgn.bindings");
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(sbgn, stream);
}
catch (JAXBException e) {
throw new RuntimeException("writeSBGN: JAXB marshalling failed", e);
}
}
/**
* Creates an SBGN object from the given BioPAX L3 model.
*
* Currently, it converts physical entities (interaction participants)
* and Conversion, Control, including their sub-classes, TemplateReaction,
* MolecularInteraction, GeneticInteraction, and base Interaction.class types.
* I does not convert other BioPAX entities and utility classes, such as:
* Pathway, Evidence, PathwayStep.
*
* @param model model to convert to SBGN
* @return SBGN representation of the BioPAX model
*/
public Sbgn createSBGN(Model model)
{
assert model.getLevel().equals(BioPAXLevel.L3) : "This method only supports L3 graphs";
glyphMap = new HashMap();
compartmentMap = new HashMap();
arcMap = new HashMap();
ubiqueSet = new HashSet();
int n = 0; //approximate number of SBGN nodes
// Create glyphs for Physical Entities
for (Entity entity : model.getObjects(Entity.class))
{
if (needsToBeCreatedInitially(entity))
{
createGlyph(entity);
++n;
}
}
// Create glyph for conversions and link with arcs
for (Interaction interaction : model.getObjects(Interaction.class))
{
if(interaction.getParticipant().isEmpty())
continue;
// For each conversion we check if we need to create a left-to-right and/or right-to-left process.
if(interaction instanceof Conversion) {
Conversion conv = (Conversion) interaction;
if (conv.getConversionDirection() == null ||
conv.getConversionDirection().equals(ConversionDirectionType.LEFT_TO_RIGHT) ||
(conv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE) &&
useTwoGlyphsForReversibleConversion)) {
createProcessAndConnections(conv, ConversionDirectionType.LEFT_TO_RIGHT);
} else if (conv.getConversionDirection() != null &&
(conv.getConversionDirection().equals(ConversionDirectionType.RIGHT_TO_LEFT) ||
(conv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE)) &&
useTwoGlyphsForReversibleConversion)) {
createProcessAndConnections(conv, ConversionDirectionType.RIGHT_TO_LEFT);
} else if (conv.getConversionDirection() != null &&
conv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE) &&
!useTwoGlyphsForReversibleConversion) {
createProcessAndConnections(conv, ConversionDirectionType.REVERSIBLE);
}
} else if(interaction instanceof TemplateReaction) {
// Create glyph for template reactions and link with arcs
createProcessAndConnections((TemplateReaction) interaction);
} else if(interaction instanceof MolecularInteraction) {
createMiProcess((MolecularInteraction) interaction);
} else if(interaction instanceof GeneticInteraction) {
createGiProcess((GeneticInteraction) interaction);
} else if(!(interaction instanceof Control)) {
createBasicProcess(interaction);
}
++n;
}
// Register created objects into sbgn construct
final Sbgn sbgn = factory.createSbgn();
org.sbgn.bindings.Map map = new org.sbgn.bindings.Map();
sbgn.setMap(map);
map.setLanguage(Language.PD.toString());
map.getGlyph().addAll(getRootGlyphs(glyphMap.values()));
map.getGlyph().addAll(getRootGlyphs(ubiqueSet));
map.getGlyph().addAll(compartmentMap.values());
map.getArc().addAll(arcMap.values());
//TODO: store the BioPAX model's metadata (e.g., uri, name, provenance) in the SBGN Notes in JSON format
//experimental - storing metadata as custom XML within Notes element
// biopaxMetaDoc.setDocumentURI(model.getUri()); //can be null
// Element elt = biopaxMetaDoc.createElementNS(BIOPAX_NS, "Model");
// elt.setPrefix("bp");
// elt.setAttribute("name", model.getName()); //can be null/empty too
// SBGNBase.Notes modelNotes = new SBGNBase.Notes();
// sbgn.setNotes(modelNotes);
// modelNotes.getAny().add(elt);
final boolean layout = doLayout && n < this.maxNodes && !arcMap.isEmpty();
try {
//Must call this, although actual layout might never run;
//in some real data tests, skipping createLayout method
//led to malformed SBGN model, unfortunately...
(new SBGNLayoutManager()).createLayout(sbgn, layout);
} catch (Exception e) {
throw new RuntimeException("SBGN Layout of " + model.getXmlBase()
+ ((model.getName()==null) ? "" : model.getName()) + " failed.", e);
}
if(!layout) log.warn(String.format("No layout, for either " +
"it's disabled: %s, or ~ no. nodes > %s: %s, or - no edges: %s",
!doLayout, maxNodes, n>maxNodes, arcMap.isEmpty()));
return sbgn; //modified sbgn (even when no layout is run)
}
// Associate controllers
private void processControllers(Set controls, Glyph process)
{
for (Control ctrl : controls) {
//TODO unsure about whether to skip mapping a Control when there's direction mismatch (keep all for now)
// // If there is a direction mismatch between the process and the control, just skip it
// if (ctrl instanceof Catalysis) {
// CatalysisDirectionType catDir = ((Catalysis) ctrl).getCatalysisDirection();
// if (catDir != null) {
// if ((catDir.equals(CatalysisDirectionType.LEFT_TO_RIGHT) &&
// direction.equals(ConversionDirectionType.RIGHT_TO_LEFT)) ||
// (catDir.equals(CatalysisDirectionType.RIGHT_TO_LEFT) &&
// direction.equals(ConversionDirectionType.LEFT_TO_RIGHT))) {
// // Skip
// continue;
// }
// }
// }
//TODO: merge/avoid duplicate eges/nodes
Glyph g = createControlStructure(ctrl);
if (g != null) createArc(g, process, getControlType(ctrl), null);
}
}
/**
* Initially, we don't want to represent every PhysicalEntity or Gene node.
* For example, if a Complex is nested under another Complex,
* and if it is not a participant of any interaction,
* then we don't want to draw it separately; also skip for dangling entities.
*
* @param ent physical entity or gene (it returns false for other entity types) to test
* @return true if we want to draw this entity in SBGN; false - otherwise, or - to be auto-created later
*/
private boolean needsToBeCreatedInitially(Entity ent)
{
boolean create = false;
if(ent instanceof PhysicalEntity || ent instanceof Gene) {
if(ubiqueDet != null && ubiqueDet.isUbique(ent))
create = false; // ubiques will be created where they are actually used.
else if (!ent.getParticipantOf().isEmpty())
create = true;
else if(ent instanceof Complex && ((Complex) ent).getComponentOf().isEmpty()
&& ((Complex) ent).getMemberPhysicalEntityOf().isEmpty())
create = true; //do make a root/top complex despite it's dangling
}
return create;
}
/**
* Creates a glyph representing the given PhysicalEntity.
*
* @param e PhysicalEntity or Gene to represent
* @return the created glyph
*/
private Glyph createGlyph(Entity e)
{
String id = convertID(e.getUri());
if (glyphMap.containsKey(id))
return glyphMap.get(id);
// Create its glyph and register
Glyph g = createGlyphBasics(e, true);
glyphMap.put(g.getId(), g);
//TODO: export metadata (e.g., from bpe.getAnnotations() map) using the SBGN Extension feature
// SBGNBase.Extension ext = new SBGNBase.Extension();
// g.setExtension(ext);
// Element el = biopaxMetaDoc.createElementNS(BIOPAX_NS, e.getModelInterface().getSimpleName());
// el.setPrefix("bp");
// el.setAttribute("uri", e.getUri());
// ext.getAny().add(el);
if (g.getClone() != null)
ubiqueSet.add(g);
if(e instanceof PhysicalEntity) {
PhysicalEntity pe = (PhysicalEntity) e;
assignLocation(pe, g);
if("or".equalsIgnoreCase(g.getClazz())) {
buildGeneric(pe, g, null);
} else if (pe instanceof Complex) {
createComplexContent((Complex) pe, g);
}
}
return g;
}
/**
* Assigns compartmentRef of the glyph.
* @param pe Related PhysicalEntity
* @param g the glyph
*/
private void assignLocation(PhysicalEntity pe, Glyph g)
{
// Create compartment -- add this inside the compartment
Glyph loc = getCompartment(pe);
if (loc != null)
{
g.setCompartmentRef(loc);
}
}
/**
* This method creates a glyph for the given PhysicalEntity, sets its title and state variables
* if applicable.
*
* @param e PhysicalEntity or Gene to represent
* @param idIsFinal if ID is final, then it is recorded for future reference
* @return the glyph
*/
private Glyph createGlyphBasics(Entity e, boolean idIsFinal)
{
Glyph g = factory.createGlyph();
g.setId(convertID(e.getUri()));
String s = typeMatchMap.get(e.getModelInterface());
if(( //use 'or' sbgn class for special generic physical entities
e instanceof Complex && !((Complex)e).getMemberPhysicalEntity().isEmpty()
&& ((Complex) e).getComponent().isEmpty())
||
(e instanceof SimplePhysicalEntity && ((SimplePhysicalEntity) e).getEntityReference()==null
&& !((SimplePhysicalEntity) e).getMemberPhysicalEntity().isEmpty()))
{
s = OR.getClazz();
}
g.setClazz(s);
// Set the label
Label label = factory.createLabel();
label.setText(findLabelFor(e));
g.setLabel(label);
// Detect if ubique
if (ubiqueDet != null && ubiqueDet.isUbique(e))
{
g.setClone(factory.createGlyphClone());
}
// Put on state variables
if (!g.getClazz().equals(OR.getClazz()))
{
g.getGlyph().addAll(getInformation(e));
}
// Record the mapping
if (idIsFinal)
{
Set uris = new HashSet();
uris.add(e.getUri());
sbgn2BPMap.put(g.getId(), uris);
}
return g;
}
/**
* Gets the representing glyph of the PhysicalEntity or Gene.
* @param e PhysicalEntity or Gene to get its glyph
* @param linkID Edge id, used if the Entity is ubique
* @return Representing glyph
*/
private Glyph getGlyphToLink(Entity e, String linkID)
{
if (ubiqueDet == null || !ubiqueDet.isUbique(e))
{
return glyphMap.get(convertID(e.getUri()));
}
else {
// Create a new glyph for each use of ubique
Glyph g = createGlyphBasics(e, false);
g.setId(convertID(e.getUri()) + "_" + ModelUtils.md5hex(linkID));
Set uris = new HashSet();
uris.add(e.getUri());
sbgn2BPMap.put(g.getId(), uris);
if(e instanceof PhysicalEntity && ((PhysicalEntity)e).getCellularLocation() != null) {
assignLocation((PhysicalEntity) e, g);
}
ubiqueSet.add(g);
return g;
}
}
/**
* Fills in the content of a complex.
*
* @param cx Complex to be filled
* @param cg its glyph
*/
private void createComplexContent(Complex cx, Glyph cg)
{
if (flattenComplexContent)
{
for (PhysicalEntity mem : getFlattenedMembers(cx))
{
createComplexMember(mem, cg);
}
}
else {
for (PhysicalEntity mem : cx.getComponent())
{
if (mem instanceof Complex)
{
addComplexAsMember((Complex) mem, cg);
}
else
{
createComplexMember(mem, cg);
}
}
}
}
private void buildGeneric(PhysicalEntity generic, Glyph or, Glyph container)
{
assert "or".equalsIgnoreCase(or.getClazz()) : "must be 'or' glyph class";
for (PhysicalEntity m : generic.getMemberPhysicalEntity())
{
Glyph g = createGlyphBasics(m,false);
if(container!=null)
container.getGlyph().add(g);
String gid = g.getId() + "_" + ModelUtils.md5hex("memberof_" + or.getId());
g.setId(gid);
glyphMap.put(gid, g);
Set uris = new HashSet();
uris.add(m.getUri());
sbgn2BPMap.put(gid, uris);
assignLocation(m, g);
createArc(g, or, LOGIC_ARC.getClazz(), null);
if(m instanceof Complex)
createComplexContent((Complex) m, g);
}
}
/**
* Recursive method for creating the content of a complex. A complex may contain other complexes
* (bad practice), but SBGN needs them flattened. If an inner complex is empty, then we
* represent it using a glyph. Otherwise we represent only the members of this inner complex,
* merging them with the most outer complex.
*
* @param cx inner complex to add as member
* @param container glyph for most outer complex
*/
private void addComplexAsMember(Complex cx, Glyph container)
{
// Create a glyph for the inner complex
Glyph inner = createComplexMember(cx, container);
for (PhysicalEntity mem : cx.getComponent())
{
if (mem instanceof Complex)
{
// Recursive call for inner complexes
addComplexAsMember((Complex) mem, inner);
}
else
{
createComplexMember(mem, inner);
}
}
}
/**
* Gets the members of the Complex that needs to be displayed in a flattened view.
* @param cx to get members
* @return members to display
*/
private Set getFlattenedMembers(Complex cx)
{
Set set = new HashSet();
for (PhysicalEntity mem : cx.getComponent())
{
if (mem instanceof Complex)
{
if (!hasNonComplexMember((Complex) mem))
{
set.add(mem);
}
else
{
set.addAll(getFlattenedMembers((Complex) mem));
}
}
else
set.add(mem);
}
return set;
}
/**
* Checks if a Complex contains any PhysicalEntity member which is not a Complex.
* @param cx to check
* @return true if there is a non-complex member
*/
private boolean hasNonComplexMember(Complex cx)
{
for (PhysicalEntity mem : cx.getComponent())
{
if (! (mem instanceof Complex)) return true;
else
{
if (hasNonComplexMember((Complex) mem)) return true;
}
}
return false;
}
/**
* Creates a glyph for the complex member.
*
* @param pe PhysicalEntity to represent as complex member
* @param container Glyph for the complex shell
*/
private Glyph createComplexMember(PhysicalEntity pe, Glyph container)
{
Glyph g = createGlyphBasics(pe, false);
container.getGlyph().add(g);
// A PhysicalEntity may appear in many complexes -- we identify the member using its complex
g.setId(g.getId() + "_" + ModelUtils.md5hex(container.getId()));
glyphMap.put(g.getId(), g);
Set uris = new HashSet();
uris.add(pe.getUri());
sbgn2BPMap.put(g.getId(), uris);
if("or".equalsIgnoreCase(g.getClazz())) {
buildGeneric(pe, g, container);
}
return g;
}
/**
* Looks for the display name of this PhysicalEntity. If there is none, then it looks for the
* display name of its EntityReference. If still no name at hand, it tries the standard
* name, and then first element in name lists.
*
* A good BioPAX file will use a short and specific name (like HGNC symbols) as displayName.
*
* @param pe PhysicalEntity or Gene to find a name
* @return a name for labeling
*/
private String findLabelFor(Entity pe)
{
// Use gene symbol of PE
for (Xref xref : pe.getXref())
{
String sym = extractGeneSymbol(xref);
if (sym != null) return sym;
}
// Use gene symbol of ER
EntityReference er = null;
if (pe instanceof SimplePhysicalEntity)
{
er = ((SimplePhysicalEntity) pe).getEntityReference();
}
if (er != null)
{
for (Xref xref : er.getXref())
{
String sym = extractGeneSymbol(xref);
if (sym != null) return sym;
}
}
// Use display name of entity
String name = pe.getDisplayName();
if (name == null || name.trim().isEmpty())
{
if (er != null)
{
// Use display name of reference
name = er.getDisplayName();
}
if (name == null || name.trim().isEmpty())
{
// Use standard name of entity
name = pe.getStandardName();
if (name == null || name.trim().isEmpty())
{
if (er != null)
{
// Use standard name of reference
name = er.getStandardName();
}
if (name == null || name.trim().isEmpty())
{
if (!pe.getName().isEmpty())
{
// Use first name of entity
name = pe.getName().iterator().next();
}
else if (er != null && !er.getName().isEmpty())
{
// Use first name of reference
name = er.getName().iterator().next();
}
}
}
}
}
// Search for the shortest name of chemicals
if (pe instanceof SmallMolecule)
{
String shortName = getShortestName((SmallMolecule) pe);
if (shortName != null)
{
if (name == null || (shortName.length() < name.length() &&
!shortName.isEmpty()))
{
name = shortName;
}
}
}
if (name == null || name.trim().isEmpty())
{
// Don't leave it without a name
name = "noname";
}
return name;
}
/**
* Searches for the shortest name of the PhysicalEntity.
* @param spe entity to search in
* @return the shortest name
*/
private String getShortestName(SimplePhysicalEntity spe)
{
String name = null;
for (String s : spe.getName())
{
if (name == null || s.length() > name.length()) name = s;
}
EntityReference er = spe.getEntityReference();
if (er != null)
{
for (String s : er.getName())
{
if (name == null || s.length() > name.length()) name = s;
}
}
return name;
}
/**
* Searches for gene symbol in Xref.
* @param xref Xref to search
* @return gene symbol
*/
private String extractGeneSymbol(Xref xref)
{
if (xref.getDb() != null && (
xref.getDb().equalsIgnoreCase("HGNC Symbol") ||
xref.getDb().equalsIgnoreCase("Gene Symbol") ||
xref.getDb().equalsIgnoreCase("HGNC")))
{
String ref = xref.getId();
if (ref != null)
{
ref = ref.trim();
if (ref.contains(":")) ref = ref.substring(ref.indexOf(":") + 1);
if (ref.contains("_")) ref = ref.substring(ref.indexOf("_") + 1);
// if the reference is an HGNC ID, then convert it to a symbol
if (!HGNC.containsSymbol(ref) && Character.isDigit(ref.charAt(0)))
{
ref = HGNC.getSymbol(ref);
}
}
return ref;
}
return null;
}
/**
* Adds molecule type, and iterates over features of the entity and creates corresponding state
* variables. Ignores binding features and covalent-binding features.
*
* @param e entity or gene to collect features
* @return list of state variables
*/
private List getInformation(Entity e)
{
List list = new ArrayList();
// Add the molecule type before states if this is a nucleic acid or gene
if (e instanceof NucleicAcid || e instanceof Gene)
{
Glyph g = factory.createGlyph();
g.setClazz(UNIT_OF_INFORMATION.getClazz());
Label label = factory.createLabel();
String s;
if(e instanceof Dna)
s = "mt:DNA";
else if(e instanceof DnaRegion)
s = "ct:DNA";
else if(e instanceof Rna)
s = "mt:RNA";
else if(e instanceof RnaRegion)
s = "ct:RNA";
else if(e instanceof Gene)
s = "ct:gene";
else
s = "mt:NuclAc";
label.setText(s);
g.setLabel(label);
list.add(g);
}
// Extract state variables
if(e instanceof PhysicalEntity) {
PhysicalEntity pe = (PhysicalEntity) e;
extractFeatures(pe.getFeature(), true, list);
extractFeatures(pe.getNotFeature(), false, list);
}
return list;
}
/**
* Converts the features in the given feature set. Adds a "!" in front of NOT features.
*
* @param features feature set
* @param normalFeature specifies the type of features -- normal feature = true,
* NOT feature = false
* @param list state variables
*/
private void extractFeatures(Set features, boolean normalFeature,
List list)
{
for (EntityFeature feature : features)
{
if (feature instanceof ModificationFeature || feature instanceof FragmentFeature)
{
Glyph stvar = factory.createGlyph();
stvar.setClazz(STATE_VARIABLE.getClazz());
Glyph.State state = featStrGen.createStateVar(feature, factory);
if (state != null)
{
// Add a "!" in front of NOT features
if (!normalFeature)
{
state.setValue("!" + state.getValue());
}
stvar.setState(state);
list.add(stvar);
}
}
}
}
//-- Section: Create compartments -------------------------------------------------------------|
private Glyph getCompartment(String name)
{
if (name == null)
return null;
name = name.toLowerCase();
if (compartmentMap.containsKey(name))
return compartmentMap.get(name);
Glyph comp = factory.createGlyph();
comp.setId(convertID(name));
Label label = factory.createLabel();
label.setText(name);
comp.setLabel(label);
comp.setClazz(COMPARTMENT.getClazz());
compartmentMap.put(name, comp);
return comp;
}
/**
* Gets the compartment of the given PhysicalEntity.
*
* @param pe PhysicalEntity to look for its compartment
* @return name of compartment or null if there is none
*/
private Glyph getCompartment(PhysicalEntity pe)
{
CellularLocationVocabulary cl = pe.getCellularLocation();
if (cl != null && !cl.getTerm().isEmpty())
{
String name = null;
// get a cv term,
// ignoring IDs (should not be there but happens)
for(String term : cl.getTerm()) {
term = term.toLowerCase();
if(!term.matches("(go|so|mi|bto|cl|pato|mod):")) {
name = term;
break;
}
}
return getCompartment(name);
} else
return null;
}
//-- Section: Create reactions ----------------------------------------------------------------|
/**
* Creates a representation for Conversion.
*
* @param cnv the conversion
* @param direction direction of the conversion to create
*/
private void createProcessAndConnections(Conversion cnv, ConversionDirectionType direction)
{
assert cnv.getConversionDirection() == null ||
cnv.getConversionDirection().equals(direction) ||
cnv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE);
// create the process for the conversion in that direction
Glyph process = factory.createGlyph();
process.setClazz(PROCESS.getClazz());
process.setId(convertID(cnv.getUri()) + "_" + direction.name().replaceAll("_",""));
glyphMap.put(process.getId(), process);
// Determine input and output sets
Set input = direction.equals(ConversionDirectionType.RIGHT_TO_LEFT) ?
cnv.getRight() : cnv.getLeft();
Set output = direction.equals(ConversionDirectionType.RIGHT_TO_LEFT) ?
cnv.getLeft() : cnv.getRight();
// Create input and outputs ports for the process
addPorts(process);
Map stoic = getStoichiometry(cnv);
// Associate inputs to input port
for (PhysicalEntity pe : input)
{
Glyph g = getGlyphToLink(pe, process.getId());
createArc(g, process.getPort().get(0), direction == ConversionDirectionType.REVERSIBLE ?
PRODUCTION.getClazz() : CONSUMPTION.getClazz(), stoic.get(pe));
}
// Associate outputs to output port
for (PhysicalEntity pe : output)
{
Glyph g = getGlyphToLink(pe, process.getId());
createArc(process.getPort().get(1), g, PRODUCTION.getClazz(), stoic.get(pe));
}
processControllers(cnv.getControlledOf(), process);
// Record mapping
Set uris = new HashSet();
uris.add(cnv.getUri());
sbgn2BPMap.put(process.getId(), uris);
}
/**
* Gets the map of stoichiometry coefficients of participants.
* @param conv the conversion
* @return map from physical entities to their stoichiometry
*/
private Map getStoichiometry(Conversion conv)
{
Map map = new HashMap();
for (Stoichiometry stoc : conv.getParticipantStoichiometry())
{
map.put(stoc.getPhysicalEntity(), stoc);
}
return map;
}
/**
* Creates a representation for TemplateReaction.
*
* @param tr template reaction
*/
private void createProcessAndConnections(TemplateReaction tr)
{
// create the process for the reaction
Glyph process = factory.createGlyph();
process.setClazz(PROCESS.getClazz());
process.setId(convertID(tr.getUri()));
glyphMap.put(process.getId(), process);
// Add input and output ports
addPorts(process);
final Set products = tr.getProduct();
final Set participants = new HashSet( //new modifiable set
new ClassFilterSet(tr.getParticipant(), PhysicalEntity.class));
// link products, if any
// ('participant' property is there defined sometimes instead of or in addition to 'product' or 'template')
for (PhysicalEntity pe : products)
{
Glyph g = getGlyphToLink(pe, process.getId());
createArc(process.getPort().get(1), g, PRODUCTION.getClazz(), null);
participants.remove(pe);
}
// link template, if present
PhysicalEntity template = tr.getTemplate();
if(template != null) {
Glyph g = getGlyphToLink(template, process.getId());
createArc(process.getPort().get(0), g, ArcClazz.INTERACTION.getClazz(), null);
participants.remove(template);
} else if(participants.isEmpty()) { //when no template is defined and cannot be inferred
// Create a source-and-sink as the input
Glyph sas = factory.createGlyph();
sas.setClazz(SOURCE_AND_SINK.getClazz());
sas.setId("unknown-template_" + ModelUtils.md5hex(process.getId()));
glyphMap.put(sas.getId(), sas);
createArc(sas, process.getPort().get(0), ArcClazz.INTERACTION.getClazz(), null);
}
//infer input or output type arc for other, if any, participants
for (PhysicalEntity pe : participants)
{
Glyph g = getGlyphToLink(pe, process.getId());
if(template==null)
createArc(g, process.getPort().get(0), ArcClazz.INTERACTION.getClazz(), null);
else
createArc(process.getPort().get(1), g, PRODUCTION.getClazz(), null);
}
// Associate controllers
processControllers(tr.getControlledOf(), process);
// Record mapping
sbgn2BPMap.put(process.getId(), new HashSet(Collections.singleton(tr.getUri())));
}
private void createBasicProcess(Interaction interaction)
{
if(!Interaction.class.equals(interaction.getModelInterface()))
log.warn("createdBasicProcess, actual type: " + interaction.getModelInterface().getSimpleName());
// create the process for the conversion in that direction
Glyph process = factory.createGlyph();
process.setClazz(OMITTED_PROCESS.getClazz());
process.setId(convertID(interaction.getUri()));
glyphMap.put(process.getId(), process);
addPorts(process);
// Associate participants
for (PhysicalEntity pe : new ClassFilterSet(interaction.getParticipant(), PhysicalEntity.class))
{
Glyph g = getGlyphToLink(pe, process.getId());
createArc(g, process.getPort().get(0), ArcClazz.INTERACTION.getClazz(), null);
}
// Associate controllers
processControllers(interaction.getControlledOf(), process);
// Record mapping
sbgn2BPMap.put(process.getId(), new HashSet(Collections.singleton(interaction.getUri())));
}
private void createMiProcess(MolecularInteraction interaction)
{
// create the process for the conversion in that direction
Glyph process = factory.createGlyph();
process.setClazz(ASSOCIATION.getClazz());
process.setId(convertID(interaction.getUri()));
glyphMap.put(process.getId(), process);
addPorts(process);
// Associate participants
for (PhysicalEntity pe : new ClassFilterSet(interaction.getParticipant(), PhysicalEntity.class))
{
Glyph g = getGlyphToLink(pe, process.getId());
createArc(g, process.getPort().get(0), ArcClazz.INTERACTION.getClazz(), null);
}
// Associate controllers
processControllers(interaction.getControlledOf(), process);
// Record mapping
sbgn2BPMap.put(process.getId(), new HashSet(Collections.singleton(interaction.getUri())));
}
private void createGiProcess(GeneticInteraction interaction)
{
// create the process for the conversion in that direction
Glyph process = factory.createGlyph();
process.setClazz(OMITTED_PROCESS.getClazz());
process.setId(convertID(interaction.getUri()));
glyphMap.put(process.getId(), process);
addPorts(process);
PhenotypeVocabulary v = interaction.getPhenotype();
if(v != null && !v.getTerm().isEmpty())
{
String term = v.getTerm().iterator().next().toLowerCase().trim();
String id = convertID(term);
Glyph g = glyphMap.get(id);
if(g == null) {
g = factory.createGlyph();
g.setId(id);
g.setClazz(PHENOTYPE.getClazz());
Label label = factory.createLabel();
label.setText(term);
g.setLabel(label);
glyphMap.put(g.getId(), g);
}
createArc(process.getPort().get(1), g, ArcClazz.STIMULATION.getClazz(), null);
}
// Associate participants
for (Entity e : interaction.getParticipant())
{
Gene gene = (Gene)e; //safe as a GI can only contain Gene participants
Glyph g = getGlyphToLink(gene, process.getId());
createArc(g, process.getPort().get(0), ArcClazz.INTERACTION.getClazz(), null);
}
// Associate controllers
processControllers(interaction.getControlledOf(), process);
// Record mapping
sbgn2BPMap.put(process.getId(), new HashSet(Collections.singleton(interaction.getUri())));
}
/**
* Creates or gets the glyph to connect to the control arc.
*
* @param ctrl Control to represent
* @return glyph representing the controller tree
*/
private Glyph createControlStructure(Control ctrl)
{
Glyph cg;
Set controllers = getControllers(ctrl);
// If no representable controller found, skip this control
if (controllers.isEmpty())
{
cg = null;
}
else if (controllers.size() == 1 && getControllerSize(ctrl.getControlledOf()) == 0)
{ // If there is only one controller with no modulator, put an arc for controller
cg = getGlyphToLink(controllers.iterator().next(), convertID(ctrl.getUri()));
}
else
{
// This list will contain handles for each participant of the AND structure
List toConnect = new ArrayList();
// Bundle controllers if necessary
Glyph gg = handlePEGroup(controllers, convertID(ctrl.getUri()));
if(gg != null)
toConnect.add(gg);
// Create handles for each controller
for (Control ctrl2 : ctrl.getControlledOf())
{
Glyph g = createControlStructure(ctrl2);
if (g != null)
{
// If the control is negative, add a NOT in front of it
if (getControlType(ctrl2).equals(INHIBITION.getClazz()))
{
g = addNOT(g);
}
toConnect.add(g);
}
}
// Handle co-factors of catalysis
if (ctrl instanceof Catalysis)
{
Set cofs = ((Catalysis) ctrl).getCofactor();
Glyph g = handlePEGroup(cofs, convertID(ctrl.getUri()));
if (g != null)
toConnect.add(g);
}
if (toConnect.isEmpty()) {
return null;
}
else if (toConnect.size() == 1)
{
cg = toConnect.iterator().next();
}
else
{
cg = connectWithAND(toConnect);
}
}
return cg;
}
/**
* Prepares the necessary construct for adding the given PhysicalEntity set to the Control
* being drawn.
*
* @param pes entities to use in control
* @return the glyph to connect to the appropriate place
*/
private Glyph handlePEGroup(Set pes, String context)
{
int sz = pes.size();
if (sz > 1)
{
List gs = getGlyphsOfPEs(pes, context);
return connectWithAND(gs);
}
else if (sz == 1) {
PhysicalEntity pe = pes.iterator().next();
if(glyphMap.containsKey(convertID(pe.getUri())))
return getGlyphToLink(pe, context);
}
//'pes' was empty
return null;
}
/**
* Gets the glyphs of the given set of PhysicalEntity objects. Does not create anything.
*
* @param pes entities to get their glyphs
* @return glyphs of entities
*/
private List getGlyphsOfPEs(Set pes, String context)
{
List gs = new ArrayList();
for (PhysicalEntity pe : pes)
{
if (glyphMap.containsKey(convertID(pe.getUri())))
{
gs.add(getGlyphToLink(pe, context));
}
}
return gs;
}
/**
* Creates an AND glyph downstream of the given glyphs.
*
* @param gs upstream glyph list
* @return AND glyph
*/
private Glyph connectWithAND(List gs)
{
// Compose an ID for the AND glyph
StringBuilder sb = new StringBuilder();
Iterator iterator = gs.iterator();
if(iterator.hasNext())
sb.append(iterator.next());
while (iterator.hasNext()) {
sb.append("-AND-").append(iterator.next().getId());
}
String id = ModelUtils.md5hex(sb.toString()); //shorten a very long id
// Create the AND glyph if not exists
Glyph and = glyphMap.get(id);
if (and == null) {
and = factory.createGlyph();
and.setClazz(AND.getClazz());
and.setId(id);
glyphMap.put(and.getId(), and);
}
// Connect upstream to the AND glyph
for (Glyph g : gs) {
createArc(g, and, LOGIC_ARC.getClazz(), null);
}
return and;
}
/**
* Adds a NOT glyph next to the given glyph.
*
* @param g glyph to add NOT
* @return NOT glyph
*/
private Glyph addNOT(Glyph g)
{
// Assemble an ID for the NOT glyph
String id = "NOT-" + g.getId();
// Find or create the NOT glyph
Glyph not = glyphMap.get(id);
if (not == null) {
not = factory.createGlyph();
not.setId(id);
not.setClazz(NOT.getClazz());
glyphMap.put(not.getId(), not);
}
// Connect the glyph and NOT
createArc(g, not, LOGIC_ARC.getClazz(), null);
return not;
}
/**
* Converts the control type of the Control to the SBGN classes.
*
* @param ctrl Control to get its type
* @return SBGN type of the Control
*/
private String getControlType(Control ctrl)
{
if (ctrl instanceof Catalysis)
{
// Catalysis has its own class
return CATALYSIS.getClazz();
}
ControlType type = ctrl.getControlType();
if (type == null)
{
// Use stimulation as the default control type
return STIMULATION.getClazz();
}
// Map control type to stimulation or inhibition
switch (type)
{
case ACTIVATION:
case ACTIVATION_ALLOSTERIC:
case ACTIVATION_NONALLOSTERIC:
case ACTIVATION_UNKMECH:
return STIMULATION.getClazz();
case INHIBITION:
case INHIBITION_ALLOSTERIC:
case INHIBITION_OTHER:
case INHIBITION_UNKMECH:
case INHIBITION_COMPETITIVE:
case INHIBITION_IRREVERSIBLE:
case INHIBITION_UNCOMPETITIVE:
case INHIBITION_NONCOMPETITIVE:
return INHIBITION.getClazz();
}
throw new RuntimeException("Invalid control type: " + type);
}
/**
* Gets the size of representable Controller of this set of Controls.
*
* @param ctrlSet Controls to check their controllers
* @return size of representable controllers
*/
private int getControllerSize(Set ctrlSet)
{
int size = 0;
for (Control ctrl : ctrlSet)
{
size += getControllers(ctrl).size();
}
return size;
}
/**
* Gets the size of representable Controller of this Control.
*
* @param ctrl Control to check its controllers
* @return size of representable controllers
*/
private Set getControllers(Control ctrl)
{
Set controllers = new HashSet();
for (Controller clr : ctrl.getController())
{
if (clr instanceof PhysicalEntity && glyphMap.containsKey(convertID(clr.getUri())))
{
controllers.add((PhysicalEntity) clr);
}
}
return controllers;
}
/**
* Adds input and output ports to the glyph.
*
* @param g glyph to add ports
*/
private void addPorts(Glyph g)
{
Port inputPort = factory.createPort();
Port outputPort = factory.createPort();
inputPort.setId("INP_" + g.getId());
outputPort.setId("OUT_" + g.getId());
g.getPort().add(inputPort);
g.getPort().add(outputPort);
}
//-- Section: Create arcs ---------------------------------------------------------------------|
/*
* Creates an arc from the source to the target, and sets its class to the specified clazz.
* Puts the new arc in the sullied arcMap.
*
* @param source source of the arc -- either Glyph or Port
* @param target target of the arc -- either Glyph or Port
* @param clazz class of the arc
*/
private void createArc(Object source, Object target, String clazz, Stoichiometry stoic)
{
assert source instanceof Glyph || source instanceof Port : "source = " + source;
assert target instanceof Glyph || target instanceof Port : "target = " + target;
Arc arc = factory.createArc();
arc.setSource(source);
arc.setTarget(target);
arc.setClazz(clazz);
String sourceID = source instanceof Glyph ?
((Glyph) source).getId() : ((Port) source).getId();
String targetID = target instanceof Glyph ?
((Glyph) target).getId() : ((Port) target).getId();
arc.setId(sourceID + "--TO--" + targetID);
if (stoic != null && stoic.getStoichiometricCoefficient() > 1)
{
Glyph card = factory.createGlyph();
card.setClazz(CARDINALITY.getClazz());
Label label = factory.createLabel();
label.setText(new DecimalFormat("0.##").format(stoic.getStoichiometricCoefficient()));
card.setLabel(label);
arc.getGlyph().add(card);
// card.setId(convertID(stoic.getUri()+"_"+arc.getId()));
}
Arc.Start start = new Arc.Start();
start.setX(0);
start.setY(0);
arc.setStart(start);
Arc.End end = new Arc.End();
end.setX(0);
end.setY(0);
arc.setEnd(end);
arcMap.put(arc.getId(), arc);
}
/**
* Collects root-level glyphs in the given glyph collection.
*
* @param glyphCol glyph collection to search
* @return set of roots
*/
private Set getRootGlyphs(Collection glyphCol)
{
Set root = new HashSet(glyphCol);
Set children = new HashSet();
for (Glyph glyph : glyphCol)
{
addChildren(glyph, children);
}
root.removeAll(children);
return root;
}
/**
* Adds children of this glyph to the specified set recursively.
* @param glyph to collect children
* @param set to add
*/
private void addChildren(Glyph glyph, Set set)
{
for (Glyph child : glyph.getGlyph())
{
set.add(child);
addChildren(child, set);
}
}
/**
* Gets the mapping from SBGN IDs to BioPAX IDs.
* This mapping is currently many-to-one, but can
* potentially become many-to-many in the future.
* @return sbgn-to-biopax mapping
*/
public Map> getSbgn2BPMap()
{
return sbgn2BPMap;
}
private String convertID(String id)
{
//make valid XML ID - a hack; see #39; ideally would be using an equivalent to javascript encodeURI()
// return id.replaceAll("[^-\\w]", "_");
return id;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy