001package org.biopax.paxtools.io.sbgn;
002
003import org.apache.commons.logging.Log;
004import org.apache.commons.logging.LogFactory;
005import org.biopax.paxtools.io.sbgn.idmapping.HGNC;
006import org.biopax.paxtools.model.BioPAXElement;
007import org.biopax.paxtools.model.BioPAXLevel;
008import org.biopax.paxtools.model.Model;
009import org.biopax.paxtools.model.level3.*;
010import org.sbgn.Language;
011import org.sbgn.SbgnUtil;
012import org.sbgn.bindings.*;
013
014import javax.xml.bind.JAXBContext;
015import javax.xml.bind.JAXBException;
016import javax.xml.bind.Marshaller;
017import java.io.File;
018import java.io.OutputStream;
019import java.io.PrintWriter;
020import java.text.DecimalFormat;
021import java.util.*;
022import java.util.Map;
023
024import static org.sbgn.GlyphClazz.*;
025import static org.sbgn.ArcClazz.*;
026
027/**
028 * This class converts BioPAX L3 model into SBGN PD. It does not layout the objects, leaves location
029 * information unassigned.
030 *
031 * This version ignores several BioPAX L3 features during conversion:
032 * <ul>
033 * <li>Parent-child relationship between physical entities</li>
034 * <li>Parent-child relationship between entity references</li>
035 * <li>Binding features and covalent binding features of physical entities</li>
036 * </ul>
037 *
038 * Also note that:
039 * <ul>
040 * <li>Compartment is just a controlled vocabulary in BioPAX, so nesting and neighborhood relations
041 * between compartments are not handled here.</li>
042 * <li>Control structures in BioPAX and in SBGN PD are a little different. We use AND and NOT
043 * glyphs to approximate controls in BioPAX. However, ANDing everything is not really proper because
044 * BioPAX do not imply a logical operator between controllers.</li>
045 * </ul>
046 *
047 * @author Ozgun Babur
048 */
049public class L3ToSBGNPDConverter
050{
051        //-- Section: Static fields -------------------------------------------------------------------|
052
053        /**
054         * Log for logging.
055         */
056        private static final Log log = LogFactory.getLog(L3ToSBGNPDConverter.class);
057
058        /**
059         * Ubique label.
060         */
061        public static final String IS_UBIQUE = "IS_UBIQUE";
062
063        /**
064         * A matching between physical entities and SBGN classes.
065         */
066        private static Map<Class<? extends BioPAXElement>, String> typeMatchMap;
067
068        /**
069         * For creating SBGN objects.
070         */
071        private static ObjectFactory factory;
072
073        //-- Section: Instance variables --------------------------------------------------------------|
074
075        /**
076         * This class is used for detecting ubiques.
077         */
078        protected UbiqueDetector ubiqueDet;
079
080        /**
081         * This class is used for generating short printable strings (text in info boxes) from
082         * recognized entity features.
083         */
084        protected FeatureDecorator featStrGen;
085
086        /**
087         * Flag to run a layout before writing down the sbgn.
088         */
089        protected boolean doLayout;
090
091        /**
092         * Mapping from SBGN IDs to the IDs of the related objects in BioPAX.
093         */
094        protected Map<String, Set<String>> sbgn2BPMap;
095
096        /**
097         * Option to flatten nested complexes.
098         */
099        protected boolean flattenComplexContent;
100
101        /**
102         * SBGN process glyph can be used to show reversible reactions. In that case two ports of the
103         * process will only have product glyphs. However, this creates an incompatibility with BioPAX:
104         * reversible biochemical reactions can have catalysis with a direction. But if we use a single
105         * glyph for the process and direct that catalysis to it, then the direction of the catalysis
106         * will be lost. If we use two process nodes for the reversible reaction (one for left-to-right
107         * and another for right-to-left), then we can direct the directed catalysis to only the
108         * relevant process glyph.
109         *
110         * Also note that the layout do not support ports. If layout is run, the ports are removed. In
111         * that case it will be impossible to distinguish left and right components of the reversible
112         * process glyph because they will all be product edges.
113         */
114        protected boolean useTwoGlyphsForReversibleConversion;
115
116        /**
117         * ID to glyph map.
118         */
119        Map<String, Glyph> glyphMap;
120
121        /**
122         * ID to Arc map
123         */
124        Map<String, Arc> arcMap;
125
126        /**
127         * ID to compartment map.
128         */
129        Map<String, Glyph> compartmentMap;
130
131        /**
132         * Set of ubiquitous molecules.
133         */
134        Set<Glyph> ubiqueSet;
135
136        //-- Section: Public methods ------------------------------------------------------------------|
137
138        /**
139         * Empty constructor.
140         */
141        public L3ToSBGNPDConverter()
142        {
143                this(null, null, true);
144        }
145
146        /**
147         * Constructor with parameters.
148         * @param ubiqueDet Ubique detector class
149         * @param featStrGen feature string generator class
150         * @param doLayout whether we want to perform layout after SBGN creation.
151         */
152        public L3ToSBGNPDConverter(UbiqueDetector ubiqueDet, FeatureDecorator featStrGen,
153                boolean doLayout)
154        {
155                this.ubiqueDet = ubiqueDet;             
156                this.featStrGen = featStrGen;
157                this.doLayout = doLayout;
158                
159                if (this.featStrGen == null)
160                        this.featStrGen = new CommonFeatureStringGenerator();
161
162                this.useTwoGlyphsForReversibleConversion = true;
163                this.sbgn2BPMap = new HashMap<String, Set<String>>();
164                this.flattenComplexContent = true;
165        }
166
167        /**
168         * Getter class for the parameter useTwoGlyphsForReversibleConversion.
169         * @return whether use two glyphs for the reversible conversion
170         */
171        public boolean isUseTwoGlyphsForReversibleConversion()
172        {
173                return useTwoGlyphsForReversibleConversion;
174        }
175
176        /**
177         * Sets the option to use two glyphs for the reversible conversion.
178         * @param useTwoGlyphsForReversibleConversion give true if use two glyphs
179         */
180        public void setUseTwoGlyphsForReversibleConversion(boolean useTwoGlyphsForReversibleConversion)
181        {
182                this.useTwoGlyphsForReversibleConversion = useTwoGlyphsForReversibleConversion;
183        }
184
185        public boolean isFlattenComplexContent()
186        {
187                return flattenComplexContent;
188        }
189
190        public void setFlattenComplexContent(boolean flattenComplexContent)
191        {
192                this.flattenComplexContent = flattenComplexContent;
193        }
194
195        /**
196         * Converts the given model to SBGN, and writes in the specified file.
197         *
198         * @param model model to convert
199         * @param file file to write
200         */
201        public void writeSBGN(Model model, String file)
202        {
203                // Create the model
204                Sbgn sbgn = createSBGN(model);
205
206                // Write in file
207
208                try
209                {
210                        SbgnUtil.writeToFile(sbgn, new File(file));
211                }
212                catch (JAXBException e)
213                {
214                        if (log.isErrorEnabled()) log.error(e.getCause(), e);
215                }
216        }
217
218        /**
219         * Converts the given model to SBGN, and writes in the specified output stream.
220         *
221         * @param model model to convert
222         * @param stream output stream to write
223         */
224        public void writeSBGN(Model model, OutputStream stream)
225        {
226                // Create the model
227                Sbgn sbgn = createSBGN(model);
228
229                // Write in file
230
231                try
232                {
233                        sbgn.toString();
234                        JAXBContext context = JAXBContext.newInstance("org.sbgn.bindings");
235                        Marshaller marshaller = context.createMarshaller();
236                        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
237                        marshaller.marshal(sbgn, stream);
238                }
239                catch (JAXBException e)
240                {
241                        if (log.isErrorEnabled()) log.error(e.getCause(), e);
242                        e.printStackTrace(new PrintWriter(stream));
243                }
244        }
245
246        /**
247         * Creates an Sbgn object from the given model.
248         *
249         * @param model model to convert to SBGN
250         * @return SBGN representation of the model
251         */
252        public Sbgn createSBGN(Model model)
253        {
254                assert model.getLevel().equals(BioPAXLevel.L3) : "This method only supports L3 graphs";
255
256                glyphMap = new HashMap<String, Glyph>();
257                compartmentMap = new HashMap<String, Glyph>();
258                arcMap = new HashMap<String, Arc>();
259                ubiqueSet = new HashSet<Glyph>();
260
261                // Create glyphs for Physical Entities
262
263                for (PhysicalEntity entity : model.getObjects(PhysicalEntity.class))
264                {
265                        if (needsToBeCreatedInitially(entity))
266                        {
267                                createGlyph(entity);
268                        }
269                }
270
271                // Create glyph for conversions and link with arcs
272
273                for (Conversion conv : model.getObjects(Conversion.class))
274                {
275                        // For each conversion we check if we need to create a left-to-right and/or
276                        // right-to-left process.
277
278                        if (conv.getConversionDirection() == null ||
279                                conv.getConversionDirection().equals(ConversionDirectionType.LEFT_TO_RIGHT) ||
280                                (conv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE) &&
281                                useTwoGlyphsForReversibleConversion))
282                        {
283                                createProcessAndConnections(conv, ConversionDirectionType.LEFT_TO_RIGHT);
284                        }
285
286                        if (conv.getConversionDirection() != null &&
287                                (conv.getConversionDirection().equals(ConversionDirectionType.RIGHT_TO_LEFT) ||
288                                (conv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE)) &&
289                                useTwoGlyphsForReversibleConversion))
290                        {
291                                createProcessAndConnections(conv, ConversionDirectionType.RIGHT_TO_LEFT);
292                        }
293
294                        if (conv.getConversionDirection() != null &&
295                                conv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE) &&
296                                !useTwoGlyphsForReversibleConversion)
297                        {
298                                createProcessAndConnections(conv, ConversionDirectionType.REVERSIBLE);
299                        }
300                }
301
302                // Create glyph for template reactions and link with arcs
303
304                for (TemplateReaction tr : model.getObjects(TemplateReaction.class))
305                {
306                        createProcessAndConnections(tr);
307                }
308
309                // Register created objects into sbgn construct
310
311                Sbgn sbgn = factory.createSbgn();
312                org.sbgn.bindings.Map map = new org.sbgn.bindings.Map();
313                sbgn.setMap(map);
314                map.setLanguage(Language.PD.toString());
315                
316                map.getGlyph().addAll(getRootGlyphs(glyphMap.values()));
317                map.getGlyph().addAll(getRootGlyphs(ubiqueSet));
318                map.getGlyph().addAll(compartmentMap.values());
319                map.getArc().addAll(arcMap.values());
320
321                if (doLayout)
322                {
323                        /*------------------ChiLay layout modification ----------------------------*/
324                        //Apply Layout to SBGN objects
325                        SBGNLayoutManager coseLayoutManager = new SBGNLayoutManager();
326                        sbgn = coseLayoutManager.createLayout(sbgn);
327                }
328                
329                return sbgn;
330        }
331
332        //-- Section: Create molecules ----------------------------------------------------------------|
333
334        /**
335         * We don't want to represent every PhysicalEntity in SBGN. For instance if a Complex is nested
336         * under another Complex, and if it is not a participant of any interaction, we don't want to
337         * draw it.
338         *
339         * @param entity physical entity to check
340         * @return true if we will draw this entity in SBGN
341         */
342        private boolean needsToBeCreatedInitially(PhysicalEntity entity)
343        {
344                // Inner complexes will be created during creation of the top complex
345                if (entity instanceof Complex)
346                {
347                        Complex c = (Complex) entity;
348                        if (c.getParticipantOf().isEmpty() && !c.getComponentOf().isEmpty())
349                        {
350                                return false;
351                        }
352                }
353                // Complex members will be created during creation of parent complex
354                else if (entity.getParticipantOf().isEmpty() && !entity.getComponentOf().isEmpty())
355                {
356                        return false;
357                }
358                // Ubiques will be created when they are used
359                else if (ubiqueDet != null && ubiqueDet.isUbique(entity))
360                {
361                        return false;
362                }
363                return true;
364        }
365
366        /**
367         * Creates a glyph representing the given PhysicalEntity.
368         *
369         * @param pe PhysicalEntity to represent
370         * @return the created glyph
371         */
372        private Glyph createGlyph(PhysicalEntity pe)
373        {
374                String id = convertID(pe.getRDFId());
375                if (glyphMap.containsKey(id)) return glyphMap.get(id);
376
377                // Create its glyph and register
378
379                Glyph g = createGlyphBasics(pe);
380                glyphMap.put(g.getId(), g);
381                if (g.getClone() != null) ubiqueSet.add(g);
382                
383                assignLocation(pe, g);
384
385                // Fill-in the complex members if this is a complex
386
387                if (pe instanceof Complex)
388                {
389                        createComplexContent((Complex) pe);
390                }
391
392                return g;
393        }
394
395        /**
396         * Assigns compartmentRef of the glyph.
397         * @param pe Related PhysicalEntity
398         * @param g the glyph
399         */
400        private void assignLocation(PhysicalEntity pe, Glyph g)
401        {
402                // Create compartment -- add this inside the compartment
403
404                Glyph loc = getCompartment(getCompartment(pe));
405                if (loc != null) 
406                {
407                        g.setCompartmentRef(loc);
408                }
409        }
410
411        /**
412         * This method creates a glyph for the given PhysicalEntity, sets its title and state variables
413         * if applicable.
414         *
415         * @param pe PhysicalEntity to represent
416         * @return the glyph
417         */
418        private Glyph createGlyphBasics(PhysicalEntity pe)
419        {
420                return createGlyphBasics(pe, true);
421        }
422        /**
423         * This method creates a glyph for the given PhysicalEntity, sets its title and state variables
424         * if applicable.
425         *
426         * @param pe PhysicalEntity to represent
427         * @param idIsFinal if ID is final, then it is recorded for future reference
428         * @return the glyph
429         */
430        private Glyph createGlyphBasics(PhysicalEntity pe, boolean idIsFinal)
431        {
432                String s = typeMatchMap.get(pe.getModelInterface());
433
434                Glyph g = factory.createGlyph();
435                g.setId(convertID(pe.getRDFId()));
436                g.setClazz(s);
437
438                // Set the label
439
440                Label label = factory.createLabel();
441                label.setText(findALabelForMolecule(pe));
442                g.setLabel(label);
443
444                // Detect if ubique
445
446                if (ubiqueDet != null && ubiqueDet.isUbique(pe))
447                {
448                        g.setClone(factory.createGlyphClone());
449                }
450
451                // Put on state variables
452
453                List<Glyph> states = getInformation(pe);
454                g.getGlyph().addAll(states);
455
456                // Record the mapping
457                if (idIsFinal)
458                {
459                        sbgn2BPMap.put(g.getId(), new HashSet<String>());
460                        sbgn2BPMap.get(g.getId()).add(pe.getRDFId());
461                }
462                return g;
463        }
464
465        /**
466         * Gets the representing glyph of the PhysicalEntity.
467         * @param pe PhysicalEntity to get its glyph
468         * @param linkID Edge id, used if the PhysicalEntity is ubique
469         * @return Representing glyph
470         */
471        private Glyph getGlyphToLink(PhysicalEntity pe, String linkID)
472        {
473                if (ubiqueDet == null || !ubiqueDet.isUbique(pe))
474                {
475                        return glyphMap.get(convertID(pe.getRDFId()));
476                }
477                else
478                {
479                        // Create a new glyph for each use of ubique
480                        Glyph g = createGlyphBasics(pe, false);
481                        g.setId(convertID(pe.getRDFId()) + linkID);
482
483                        sbgn2BPMap.put(g.getId(), new HashSet<String>());
484                        sbgn2BPMap.get(g.getId()).add(pe.getRDFId());
485
486                        assignLocation(pe, g);
487                        ubiqueSet.add(g);
488                        return g;
489                }
490        }
491        
492        /**
493         * Fills in the content of a complex.
494         *
495         * @param cx Complex to be filled
496         */
497        private void createComplexContent(Complex cx)
498        {
499                Glyph cg = glyphMap.get(convertID(cx.getRDFId()));
500
501                if (flattenComplexContent)
502                {
503                        for (PhysicalEntity mem : getFlattenedMembers(cx))
504                        {
505                                createComplexMember(mem, cg);
506                        }
507                }
508                else
509                {
510                        for (PhysicalEntity mem : cx.getComponent())
511                        {
512                                if (mem instanceof Complex)
513                                {
514                                        addComplexAsMember((Complex) mem, cg);
515                                }
516                                else
517                                {
518                                        createComplexMember(mem, cg);
519                                }
520                        }
521                }
522        }
523
524        /**
525         * Recursive method for creating the content of a complex. A complex may contain other complexes
526         * (bad practice), but SBGN needs them flattened. If an inner complex is empty, then we
527         * represent it using a glyph. Otherwise we represent only the members of this inner complex,
528         * merging them with the most outer complex.
529         *
530         * @param cx inner complex to add as member
531         * @param container glyph for most outer complex
532         */
533        private void addComplexAsMember(Complex cx, Glyph container)
534        {
535                // Create a glyph for the inner complex
536                Glyph inner = createComplexMember(cx, container);
537
538                for (PhysicalEntity mem : cx.getComponent())
539                {
540                        if (mem instanceof Complex)
541                        {
542                                // Recursive call for inner complexes
543                                addComplexAsMember((Complex) mem, inner);
544                        }
545                        else
546                        {
547                                createComplexMember(mem, inner);
548                        }
549                }
550        }
551
552        /**
553         * Gets the members of the Complex that needs to be displayed in a flattened view.
554         * @param cx to get members
555         * @return members to display
556         */
557        private Set<PhysicalEntity> getFlattenedMembers(Complex cx)
558        {
559                Set<PhysicalEntity> set = new HashSet<PhysicalEntity>();
560
561                for (PhysicalEntity mem : cx.getComponent())
562                {
563                        if (mem instanceof Complex)
564                        {
565                                if (!hasNonComplexMember((Complex) mem))
566                                {
567                                        set.add(mem);
568                                }
569                                else
570                                {
571                                        set.addAll(getFlattenedMembers((Complex) mem));
572                                }
573                        }
574                        else set.add(mem);
575                }
576                return set;
577        }
578
579        /**
580         * Checks if a Complex contains any PhysicalEntity member which is not a Complex.
581         * @param cx to check
582         * @return true if there is a non-complex member
583         */
584        private boolean hasNonComplexMember(Complex cx)
585        {
586                for (PhysicalEntity mem : cx.getComponent())
587                {
588                        if (! (mem instanceof Complex)) return true;
589                        else
590                        {
591                                if (hasNonComplexMember((Complex) mem)) return true;
592                        }
593                }
594                return false;
595        }
596
597        /**
598         * Creates a glyph for the complex member.
599         *
600         * @param pe PhysicalEntity to represent as complex member
601         * @param container Glyph for the complex shell
602         */
603        private Glyph createComplexMember(PhysicalEntity pe, Glyph container)
604        {
605                Glyph g = createGlyphBasics(pe, false);
606                container.getGlyph().add(g);
607
608                // A PhysicalEntity may appear in many complexes -- we identify the member using its complex
609                g.setId(g.getId() + "_" + container.getId());
610
611                glyphMap.put(g.getId(), g);
612
613                sbgn2BPMap.put(g.getId(), new HashSet<String>());
614                sbgn2BPMap.get(g.getId()).add(pe.getRDFId());
615
616                return g;
617        }
618
619        /**
620         * Looks for the display name of this PhysicalEntity. If there is none, then it looks for the
621         * display name of its EntityReference. If still no name at hand, it tries the standard
622         * name, and then first element in name lists.
623         *
624         * A good BioPAX file will use a short and specific name (like HGNC symbols) as displayName.
625         *
626         * @param pe PhysicalEntity to find a name
627         * @return a name for labeling
628         */
629        private String findALabelForMolecule(PhysicalEntity pe)
630        {
631                // Use gene symbol of PE
632
633                for (Xref xref : pe.getXref())
634                {
635                        String sym = extractGeneSymbol(xref);
636                        if (sym != null) return sym;
637                }
638
639                // Use gene symbol of ER
640
641                EntityReference er = null;
642
643                if (pe instanceof SimplePhysicalEntity)
644                {
645                        er = ((SimplePhysicalEntity) pe).getEntityReference();
646                }
647
648                if (er != null)
649                {
650                        for (Xref xref : er.getXref())
651                        {
652                                String sym = extractGeneSymbol(xref);
653                                if (sym != null) return sym;
654                        }
655                }
656                
657                // Use display name of entity
658                String name = pe.getDisplayName();
659
660                if (name == null || name.trim().isEmpty())
661                {
662                        if (er != null)
663                        {
664                                // Use display name of reference
665                                name = er.getDisplayName();
666                        }
667
668                        if (name == null || name.trim().isEmpty())
669                        {
670                                // Use standard name of entity
671                                name = pe.getStandardName();
672
673                                if (name == null || name.trim().isEmpty())
674                                {
675                                        if (er != null)
676                                        {
677                                                // Use standard name of reference
678                                                name = er.getStandardName();
679                                        }
680
681                                        if (name == null || name.trim().isEmpty())
682                                        {
683                                                if (!pe.getName().isEmpty())
684                                                {
685                                                        // Use first name of entity
686                                                        name = pe.getName().iterator().next();
687                                                }
688                                                else if (er != null && !er.getName().isEmpty())
689                                                {
690                                                        // Use first name of reference
691                                                        name = er.getName().iterator().next();
692                                                }
693                                        }
694                                }
695                        }
696                }
697
698                // Search for the shortest name of chemicals
699                if (pe instanceof SmallMolecule)
700                {
701                        String shortName = getShortestName((SmallMolecule) pe);
702
703                        if (shortName != null)
704                        {
705                                if (name == null || (shortName.length() < name.length() &&
706                                        !shortName.isEmpty()))
707                                {
708                                        name = shortName;
709                                }
710                        }
711                }
712                
713                if (name == null || name.trim().isEmpty())
714                {
715                        // Don't leave it without a name
716                        name = "noname";
717                }
718                return name;
719        }
720
721        /**
722         * Searches for the shortest name of the PhysicalEntity.
723         * @param spe entity to search in
724         * @return the shortest name
725         */
726        private String getShortestName(SimplePhysicalEntity spe)
727        {
728                String name = null;
729
730                for (String s : spe.getName())
731                {
732                        if (name == null || s.length() > name.length()) name = s;
733                }
734
735                EntityReference er = spe.getEntityReference();
736                
737                if (er != null)
738                {
739                        for (String s : er.getName())
740                        {
741                                if (name == null || s.length() > name.length()) name = s;                               
742                        }
743                }
744                return name;
745        }
746
747        /**
748         * Searches for gene symbol in Xref.
749         * @param xref Xref to search
750         * @return gene symbol
751         */
752        private String extractGeneSymbol(Xref xref)
753        {
754                if (xref.getDb() != null && (
755                        xref.getDb().equals("HGNC") ||
756                        xref.getDb().equals("Gene Symbol")))
757                {
758                        String ref = xref.getId();
759
760                        if (ref != null)
761                        {
762                                ref = ref.trim();
763                                if (ref.contains(":")) ref = ref.substring(ref.indexOf(":") + 1);
764                                if (ref.contains("_")) ref = ref.substring(ref.indexOf("_") + 1);
765
766                                if (!HGNC.containsSymbol(ref))
767                                {
768                                        ref = HGNC.getSymbol(ref);
769                                }
770                        }
771                        return ref;
772                }
773                return null;
774        }
775        
776        /**
777         * Adds molecule type, and iterates over features of the entity and creates corresponding state
778         * variables. Ignores binding features and covalent-binding features.
779         * 
780         * @param pe entity to collect features
781         * @return list of state variables
782         */
783        private List<Glyph> getInformation(PhysicalEntity pe)
784        {
785                List<Glyph> list = new ArrayList<Glyph>();
786
787                // Add the molecule type before states if this is a nucleic acid
788
789                if (pe instanceof NucleicAcid)
790                {
791                        Glyph g = factory.createGlyph();
792                        g.setClazz(UNIT_OF_INFORMATION.getClazz());
793                        Label label = factory.createLabel();
794                        String s = "mt:";
795                        s += ((pe instanceof Dna || pe instanceof DnaRegion) ? "DNA" :
796                                (pe instanceof Rna || pe instanceof RnaRegion) ? "RNA" : "NuclAc");
797                        label.setText(s);
798                        g.setLabel(label);
799                        list.add(g);
800                }
801
802                // Extract state variables
803
804                extractFeatures(pe.getFeature(), true, list);
805                extractFeatures(pe.getNotFeature(), false, list);
806
807                return list;
808        }
809
810        /**
811         * Converts the features in the given feature set. Adds a "!" in front of NOT features.
812         *
813         * @param features feature set
814         * @param normalFeature specifies the type of features -- normal feature = true,
815         *                NOT feature = false
816         * @param list state variables
817         */
818        private void extractFeatures(Set<EntityFeature> features, boolean normalFeature,
819                List<Glyph> list)
820        {
821                for (EntityFeature feature : features)
822                {
823                        if (feature instanceof ModificationFeature || feature instanceof FragmentFeature)
824                        {
825                                Glyph stvar = factory.createGlyph();
826                                stvar.setClazz(STATE_VARIABLE.getClazz());
827
828                                Glyph.State state = featStrGen.createStateVar(feature, factory);
829
830                                if (state != null)
831                                {
832                                        // Add a "!" in front of NOT features
833
834                                        if (!normalFeature)
835                                        {
836                                                state.setValue("!" + state.getValue());
837                                        }
838
839                                        stvar.setState(state);
840
841                                        list.add(stvar);
842                                }
843                        }
844                }
845        }
846
847        //-- Section: Create compartments -------------------------------------------------------------|
848
849        /**
850         * Creates or gets the compartment with the given name.
851         *
852         * @param name name of the compartment
853         * @return the compartment glyph
854         */
855        private Glyph getCompartment(String name)
856        {
857                if (name == null) return null;
858                if (compartmentMap.containsKey(name)) return compartmentMap.get(name);
859
860                Glyph comp = factory.createGlyph();
861                comp.setId(name);
862                Label label = factory.createLabel();
863                label.setText(name);
864                comp.setLabel(label);
865                comp.setClazz(COMPARTMENT.getClazz());
866
867                compartmentMap.put(name, comp);
868                return comp;
869        }
870
871        /**
872         * Gets the compartment of the given PhysicalEntity.
873         *
874         * @param pe PhysicalEntity to look for its compartment
875         * @return name of compartment or null if there is none
876         */
877        private String getCompartment(PhysicalEntity pe)
878        {
879                CellularLocationVocabulary cl = pe.getCellularLocation();
880                if (cl != null)
881                {
882                        if (!cl.getTerm().isEmpty())
883                        {
884                                return cl.getTerm().iterator().next().replaceAll(" ", "_");
885                        }
886                }
887                return null;
888        }
889
890        //-- Section: Create reactions ----------------------------------------------------------------|
891
892        /**
893         * Creates a representation for Conversion.
894         *
895         * @param cnv the conversion
896         * @param direction direction of the conversion to create
897         */
898        private void createProcessAndConnections(Conversion cnv,
899                ConversionDirectionType direction)
900        {
901                assert cnv.getConversionDirection() == null ||
902                        cnv.getConversionDirection().equals(direction) ||
903                        cnv.getConversionDirection().equals(ConversionDirectionType.REVERSIBLE);
904
905                // create the process for the conversion in that direction
906
907                Glyph process = factory.createGlyph();
908                process.setClazz(PROCESS.getClazz());
909                process.setId(convertID(cnv.getRDFId()) + direction);
910                glyphMap.put(process.getId(), process);
911
912                // Determine input and output sets
913                
914                Set<PhysicalEntity> input = direction.equals(ConversionDirectionType.RIGHT_TO_LEFT) ?
915                        cnv.getRight() : cnv.getLeft();
916                Set<PhysicalEntity> output = direction.equals(ConversionDirectionType.RIGHT_TO_LEFT) ?
917                        cnv.getLeft() : cnv.getRight();
918
919                // Create input and outputs ports for the process
920                addPorts(process);
921
922                Map<PhysicalEntity, Stoichiometry> stoic = getStoichiometry(cnv);
923
924                // Associate inputs to input port
925
926                for (PhysicalEntity pe : input)
927                {
928                        Glyph g = getGlyphToLink(pe, process.getId());
929                        createArc(g, process.getPort().get(0), direction == ConversionDirectionType.REVERSIBLE ?
930                                PRODUCTION.getClazz() : CONSUMPTION.getClazz(), stoic.get(pe));
931                }
932
933                // Associate outputs to output port
934
935                for (PhysicalEntity pe : output)
936                {
937                        Glyph g = getGlyphToLink(pe, process.getId());
938                        createArc(process.getPort().get(1), g, PRODUCTION.getClazz(), stoic.get(pe));
939                }
940
941                // Associate controllers
942
943                for (Control ctrl : cnv.getControlledOf())
944                {
945                        // If there is a direction mismatch between the process and the control, just skip it
946
947                        if (ctrl instanceof Catalysis)
948                        {
949                                CatalysisDirectionType catDir = ((Catalysis) ctrl).getCatalysisDirection();
950                                if (catDir != null)
951                                {
952                                        if ((catDir.equals(CatalysisDirectionType.LEFT_TO_RIGHT) &&
953                                                direction.equals(ConversionDirectionType.RIGHT_TO_LEFT)) ||
954                                                (catDir.equals(CatalysisDirectionType.RIGHT_TO_LEFT) &&
955                                                direction.equals(ConversionDirectionType.LEFT_TO_RIGHT)))
956                                        {
957                                                // Skip
958                                                continue;
959                                        }
960                                }
961                        }
962
963                        Glyph g = createControlStructure(ctrl);
964                        if (g != null) createArc(g, process, getControlType(ctrl), null);
965                }
966
967                // Record mapping
968
969                sbgn2BPMap.put(process.getId(), new HashSet<String>());
970                sbgn2BPMap.get(process.getId()).add(cnv.getRDFId());
971        }
972
973        /**
974         * Gets the map of stoichiometry coefficients of participants.
975         * @param conv the conversion
976         * @return map from physical entities to their stoichiometry
977         */
978        private Map<PhysicalEntity, Stoichiometry> getStoichiometry(Conversion conv)
979        {
980                Map<PhysicalEntity, Stoichiometry> map = new HashMap<PhysicalEntity, Stoichiometry>();
981                for (Stoichiometry stoc : conv.getParticipantStoichiometry())
982                {
983                        map.put(stoc.getPhysicalEntity(), stoc);
984                }
985                return map;
986        }
987
988        /**
989         * Creates a representation for TemplateReaction.
990         *
991         * @param tr template reaction
992         */
993        private void createProcessAndConnections(TemplateReaction tr)
994        {
995                // create the process for the reaction
996
997                Glyph process = factory.createGlyph();
998                process.setClazz(PROCESS.getClazz());
999                process.setId(convertID(tr.getRDFId()));
1000                glyphMap.put(process.getId(), process);
1001
1002                // Add input and output ports
1003                addPorts(process);
1004
1005                // Create a source-and-sink as the input
1006
1007                Glyph sas = factory.createGlyph();
1008                sas.setClazz(SOURCE_AND_SINK.getClazz());
1009                sas.setId("SAS_For_" + process.getId());
1010                glyphMap.put(sas.getId(), sas);
1011                createArc(sas, process.getPort().get(0), CONSUMPTION.getClazz(), null);
1012
1013                // Associate products
1014
1015                for (PhysicalEntity pe : tr.getProduct())
1016                {
1017                        Glyph g = getGlyphToLink(pe, process.getId());
1018                        createArc(process.getPort().get(1), g, PRODUCTION.getClazz(), null);
1019                }
1020
1021                // Associate controllers
1022
1023                for (Control ctrl : tr.getControlledOf())
1024                {
1025                        Glyph g = createControlStructure(ctrl);
1026                        if (g != null) createArc(g, process, getControlType(ctrl), null);
1027                }
1028
1029                // Record mapping
1030
1031                sbgn2BPMap.put(process.getId(), new HashSet<String>());
1032                sbgn2BPMap.get(process.getId()).add(tr.getRDFId());
1033        }
1034
1035        /**
1036         * Creates or gets the glyph to connect to the control arc.
1037         *
1038         * @param ctrl Control to represent
1039         * @return glyph representing the controller tree
1040         */
1041        private Glyph createControlStructure(Control ctrl)
1042        {
1043                Glyph cg;
1044
1045                Set<PhysicalEntity> controllers = getControllers(ctrl);
1046
1047                // If no representable controller found, skip this control
1048                if (controllers.isEmpty()) cg = null;
1049
1050                // If there is only one controller with no modulator, put an arc for controller
1051
1052                else if (controllers.size() == 1 && getControllerSize(ctrl.getControlledOf()) == 0)
1053                {
1054                        cg = getGlyphToLink(controllers.iterator().next(), convertID(ctrl.getRDFId()));
1055                }
1056
1057                else
1058                {
1059                        // This list will contain handles for each participant of the AND structure
1060                        List<Glyph> toConnect = new ArrayList<Glyph>();
1061
1062                        // Bundle controllers if necessary
1063
1064                        Glyph gg = handlePEGroup(controllers, convertID(ctrl.getRDFId()));
1065                        if(gg != null)
1066                                toConnect.add(gg);
1067
1068                        // Create handles for each controller
1069
1070                        for (Control ctrl2 : ctrl.getControlledOf())
1071                        {
1072                                Glyph g = createControlStructure(ctrl2);
1073                                if (g != null)
1074                                {
1075                                        // If the control is negative, add a NOT in front of it
1076
1077                                        if (getControlType(ctrl2).equals(INHIBITION.getClazz()))
1078                                        {
1079                                                g = addNOT(g);
1080                                        }
1081
1082                                        toConnect.add(g);
1083                                }
1084                        }
1085
1086                        // Handle co-factors of catalysis
1087
1088                        if (ctrl instanceof Catalysis)
1089                        {
1090                                Set<PhysicalEntity> cofs = ((Catalysis) ctrl).getCofactor();
1091                                Glyph g = handlePEGroup(cofs, convertID(ctrl.getRDFId()));
1092                                if (g != null) 
1093                                        toConnect.add(g);
1094                        }
1095
1096                        if (toConnect.isEmpty()) 
1097                                return null;
1098                        else if (toConnect.size() == 1)
1099                        {
1100                                cg = toConnect.iterator().next();
1101                        }
1102                        else
1103                        {
1104                                cg = connectWithAND(toConnect);
1105                        }
1106                }
1107
1108                return cg;
1109        }
1110
1111        /**
1112         * Prepares the necessary construct for adding the given PhysicalEntity set to the Control
1113         * being drawn.
1114         *
1115         * @param pes entities to use in control
1116         * @return the glyph to connect to the appropriate place
1117         */
1118        private Glyph handlePEGroup(Set<PhysicalEntity> pes, String context)
1119        {
1120                int sz = pes.size();            
1121                if (sz > 1)
1122                {
1123                        List<Glyph> gs = getGlyphsOfPEs(pes, context);
1124                        return connectWithAND(gs);
1125                }
1126                else if (sz == 1 && glyphMap.containsKey(convertID(pes.iterator().next().getRDFId())))
1127                {
1128                        return getGlyphToLink(pes.iterator().next(), context);
1129                }
1130                
1131                //'pes' was empty
1132                return null;
1133        }
1134        
1135        /**
1136         * Gets the glyphs of the given set of PhysicalEntity objects. Does not create anything.
1137         *
1138         * @param pes entities to get their glyphs
1139         * @return glyphs of entities
1140         */
1141        private List<Glyph> getGlyphsOfPEs(Set<PhysicalEntity> pes, String context)
1142        {
1143                List<Glyph> gs = new ArrayList<Glyph>();
1144                for (PhysicalEntity pe : pes)
1145                {
1146                        if (glyphMap.containsKey(convertID(pe.getRDFId())))
1147                        {
1148                                gs.add(getGlyphToLink(pe, context));
1149                        }
1150                }
1151                return gs;
1152        }
1153
1154        /**
1155         * Creates an AND glyph downstream of the given glyphs.
1156         *
1157         * @param gs upstream glyph list
1158         * @return AND glyph
1159         */
1160        private Glyph connectWithAND(List<Glyph> gs)
1161        {
1162                // Compose an ID for the AND glyph
1163
1164                String id = "";
1165
1166                for (Glyph g : gs)
1167                {
1168                        id = id + (id.length() > 0 ? "-AND-" : "") + g.getId();
1169                }
1170
1171                // Create the AND glyph if not exists
1172
1173                Glyph and;
1174                if (!glyphMap.containsKey(id))
1175                {
1176                        and = factory.createGlyph();
1177                        and.setClazz(AND.getClazz());
1178                        and.setId(id);
1179                        glyphMap.put(and.getId(), and);
1180                }
1181                else
1182                {
1183                        and = glyphMap.get(id);
1184                }
1185
1186                // Connect upstream to the AND glyph
1187
1188                for (Glyph g : gs)
1189                {
1190                        createArc(g, and, LOGIC_ARC.getClazz(), null);
1191                }
1192                return and;
1193        }
1194
1195        /**
1196         * Adds a NOT glyph next to the given glyph.
1197         *
1198         * @param g glyph to add NOT
1199         * @return NOT glyph
1200         */
1201        private Glyph addNOT(Glyph g)
1202        {
1203                // Assemble an ID for the NOT glyph
1204
1205                String id = "NOT-" + g.getId();
1206
1207                // Find or create the NOT glyph
1208
1209                Glyph not;
1210                if (!glyphMap.containsKey(id))
1211                {
1212                        not = factory.createGlyph();
1213                        not.setId(id);
1214                        not.setClazz(NOT.getClazz());
1215                        glyphMap.put(not.getId(), not);
1216                }
1217                else
1218                {
1219                        not = glyphMap.get(id);
1220                }
1221
1222                // Connect the glyph and NOT
1223                createArc(g, not, LOGIC_ARC.getClazz(), null);
1224
1225                return not;
1226        }
1227
1228        /**
1229         * Converts the control type of the Control to the SBGN classes.
1230         *
1231         * @param ctrl Control to get its type
1232         * @return SBGN type of the Control
1233         */
1234        private String getControlType(Control ctrl)
1235        {
1236                if (ctrl instanceof Catalysis)
1237                {
1238                        // Catalysis has its own class
1239                        return CATALYSIS.getClazz();
1240                }
1241
1242                ControlType type = ctrl.getControlType();
1243                if (type == null)
1244                {
1245                        // Use stimulation as the default control type
1246                        return STIMULATION.getClazz();
1247                }
1248
1249                // Map control type to stimulation or inhibition
1250
1251                switch (type)
1252                {
1253                        case ACTIVATION:
1254                        case ACTIVATION_ALLOSTERIC:
1255                        case ACTIVATION_NONALLOSTERIC:
1256                        case ACTIVATION_UNKMECH: return STIMULATION.getClazz();
1257                        case INHIBITION:
1258                        case INHIBITION_ALLOSTERIC:
1259                        case INHIBITION_OTHER:
1260                        case INHIBITION_UNKMECH:
1261                        case INHIBITION_COMPETITIVE:
1262                        case INHIBITION_IRREVERSIBLE:
1263                        case INHIBITION_UNCOMPETITIVE:
1264                        case INHIBITION_NONCOMPETITIVE: return INHIBITION.getClazz();
1265                }
1266                throw new RuntimeException("Invalid control type: " + type);
1267        }
1268
1269        /**
1270         * Gets the size of representable Controller of this set of Controls.
1271         *
1272         * @param ctrlSet Controls to check their controllers
1273         * @return size of representable controllers
1274         */
1275        private int getControllerSize(Set<Control> ctrlSet)
1276        {
1277                int size = 0;
1278                for (Control ctrl : ctrlSet)
1279                {
1280                        size += getControllers(ctrl).size();
1281                }
1282                return size;
1283        }
1284
1285        /**
1286         * Gets the size of representable Controller of this Control.
1287         *
1288         * @param ctrl Control to check its controllers
1289         * @return size of representable controllers
1290         */
1291        private Set<PhysicalEntity> getControllers(Control ctrl)
1292        {
1293                Set<PhysicalEntity> controllers = new HashSet<PhysicalEntity>();
1294                for (Controller clr : ctrl.getController())
1295                {
1296                        if (clr instanceof PhysicalEntity && glyphMap.containsKey(convertID(clr.getRDFId())))
1297                        {
1298                                controllers.add((PhysicalEntity) clr);
1299                        }
1300                }
1301                return controllers;
1302        }
1303
1304
1305        /**
1306         * Adds input and output ports to the glyph.
1307         *
1308         * @param g glyph to add ports
1309         */
1310        private void addPorts(Glyph g)
1311        {
1312                Port inputPort = factory.createPort();
1313                Port outputPort = factory.createPort();
1314                inputPort.setId(g.getId() + ".input");
1315                outputPort.setId(g.getId() + ".output");
1316                g.getPort().add(inputPort);
1317                g.getPort().add(outputPort);
1318        }
1319
1320        //-- Section: Create arcs ---------------------------------------------------------------------|
1321
1322        /**
1323         * Creates an arc from the source to the target, and sets its class to the specified clazz.
1324         * Puts the new arc in the sullied arcMap.
1325         *
1326         * @param source source of the arc -- either Glyph or Port
1327         * @param target target of the arc -- either Glyph or Port
1328         * @param clazz class of the arc
1329         */
1330        private void createArc(Object source, Object target, String clazz, Stoichiometry stoic)
1331        {
1332                assert source instanceof Glyph || source instanceof Port : "source = " + source;
1333                assert target instanceof Glyph || target instanceof Port : "target = " + target;
1334
1335                Arc arc = factory.createArc();
1336                arc.setSource(source);
1337                arc.setTarget(target);
1338                arc.setClazz(clazz);
1339                
1340                String sourceID = source instanceof Glyph ?
1341                        ((Glyph) source).getId() : ((Port) source).getId();
1342                String targetID = target instanceof Glyph ?
1343                        ((Glyph) target).getId() : ((Port) target).getId();
1344
1345                arc.setId(sourceID + "--to--" + targetID);
1346
1347                if (stoic != null && stoic.getStoichiometricCoefficient() != 1F)
1348                {
1349                        Glyph card = factory.createGlyph();
1350                        card.setClazz(CARDINALITY.getClazz());
1351                        Label label = factory.createLabel();
1352                        label.setText(new DecimalFormat("0.##").format(stoic.getStoichiometricCoefficient()));
1353                        card.setLabel(label);
1354                        arc.getGlyph().add(card);
1355                }
1356
1357                Arc.Start start = new Arc.Start();
1358                start.setX(0);
1359                start.setY(0);
1360                arc.setStart(start);
1361
1362                Arc.End end = new Arc.End();
1363                end.setX(0);
1364                end.setY(0);
1365                arc.setEnd(end);
1366
1367                arcMap.put(arc.getId(), arc);
1368        }
1369
1370        /**
1371         * Collects root-level glyphs in the given glyph collection.
1372         *
1373         * @param glyphCol glyph collection to search
1374         * @return set of roots
1375         */
1376        private Set<Glyph> getRootGlyphs(Collection<Glyph> glyphCol)
1377        {
1378                Set<Glyph> root = new HashSet<Glyph>(glyphCol);
1379                Set<Glyph> children = new HashSet<Glyph>();
1380
1381                for (Glyph glyph : glyphCol)
1382                {
1383                        addChildren(glyph, children);
1384                }
1385                root.removeAll(children);
1386                return root;
1387        }
1388
1389        /**
1390         * Adds children of this glyph to the specified set recursively.
1391         * @param glyph to collect children
1392         * @param set to add
1393         */
1394        private void addChildren(Glyph glyph, Set<Glyph> set)
1395        {
1396                for (Glyph child : glyph.getGlyph())
1397                {
1398                        set.add(child);
1399                        addChildren(child, set);
1400                }
1401        }
1402
1403        /**
1404         * Gets the mapping from SBGN IDs to BioPAX IDs. This mapping is currently one-to-many, but has
1405         * potential to become many-to-many in the future.
1406         * @return sbgn-to-biopax mapping
1407         */
1408        public Map<String, Set<String>> getSbgn2BPMap()
1409        {
1410                return sbgn2BPMap;
1411        }
1412
1413        private String convertID(String biopaxID)
1414        {
1415                return biopaxID.replaceAll("[^-\\w]", "_");
1416        }
1417
1418        //-- Section: Static initialization -----------------------------------------------------------|
1419
1420        /**
1421         * Initializes resources.
1422         */
1423        static
1424        {
1425                factory = new ObjectFactory();
1426
1427                typeMatchMap = new HashMap<Class<? extends BioPAXElement>, String>();
1428                typeMatchMap.put(Protein.class, MACROMOLECULE.getClazz());
1429                typeMatchMap.put(SmallMolecule.class, SIMPLE_CHEMICAL.getClazz());
1430                typeMatchMap.put(Dna.class, NUCLEIC_ACID_FEATURE.getClazz());
1431                typeMatchMap.put(Rna.class, NUCLEIC_ACID_FEATURE.getClazz());
1432                typeMatchMap.put(DnaRegion.class, NUCLEIC_ACID_FEATURE.getClazz());
1433                typeMatchMap.put(RnaRegion.class, NUCLEIC_ACID_FEATURE.getClazz());
1434                typeMatchMap.put(NucleicAcid.class, NUCLEIC_ACID_FEATURE.getClazz());
1435                typeMatchMap.put(PhysicalEntity.class, UNSPECIFIED_ENTITY.getClazz());
1436                typeMatchMap.put(SimplePhysicalEntity.class, UNSPECIFIED_ENTITY.getClazz());
1437                typeMatchMap.put(Complex.class, COMPLEX.getClazz());
1438        }
1439}