001package org.biopax.paxtools.io.sbgn;
002
003import java.util.*;
004
005import org.ivis.layout.*;
006import org.ivis.layout.sbgn.SbgnPDNode;
007import org.ivis.layout.sbgn.SbgnProcessNode;
008import org.ivis.layout.sbgn.SbgnPDLayout;
009import org.sbgn.bindings.Arc;
010import org.sbgn.bindings.Port;
011import org.sbgn.bindings.Glyph;
012import org.sbgn.bindings.Sbgn;
013import org.sbgn.bindings.Bbox;
014
015/**
016 * SBGNLayoutManager Class: Applies layout by using ChiLay to Sbgn graph generated by paxtools.
017 * @author: Istemi Bahceci
018 * */
019public class SBGNLayoutManager
020{
021    // Layout and root objects
022    private Layout layout;
023    private VCompound root;
024
025    // mapping between view and layout level
026    private HashMap <VNode, LNode> viewToLayout;
027    private HashMap <String, VNode> layoutToView;
028    private HashMap <Glyph,VNode>  glyphToVNode;
029    private HashMap <String, Glyph> idToGLyph;
030    private HashMap <String, Glyph> idToCompartmentGlyphs;
031    private HashMap <String, Glyph> portIDToOwnerGlyph;
032    private HashMap <String,Arc> idToArcs;
033
034    /**
035     * Applies CoSE layout to the given SBGN model.
036     *
037     * @param sbgn Sbgn object to which layout will be applied
038     * @return Laid out sbgn object
039     */
040    public Sbgn createLayout(Sbgn sbgn)
041    {
042        viewToLayout = new HashMap();
043        glyphToVNode = new HashMap();
044        idToGLyph = new HashMap();
045        idToCompartmentGlyphs = new HashMap();
046        portIDToOwnerGlyph = new HashMap();
047        layoutToView = new HashMap();
048        idToArcs = new HashMap<String, Arc>();
049
050        // Using Compound spring  embedder layout
051        this.layout = new SbgnPDLayout();
052
053        // This list holds the glyphs that will be deleted after corresponding glyph is added to child glyph of another glyph.
054        ArrayList <Glyph> deletedList = new ArrayList<Glyph>();
055
056        LGraphManager graphMgr = this.layout.getGraphManager();
057        LGraph lRoot = graphMgr.addRoot();
058        this.root = new VCompound(new Glyph());
059
060        // Detect compartment glyphs and put them in a hashmap, also set compartment glyphs of members of complexes.
061        for (Glyph g: sbgn.getMap().getGlyph())
062        {
063            if(g.getClazz().equals("compartment"))
064            {
065                idToCompartmentGlyphs.put(g.getId(), g);
066            }
067
068            //Add compartment ref to the all children of this node.
069            if(g.getCompartmentRef() != null)
070            {
071                Glyph compartment = (Glyph)g.getCompartmentRef();
072                setCompartmentRefForComplexMembers(g, compartment);
073            }
074        }
075
076
077        // Add glyphs to the compartment glyphs according to their "compartmentRef" field.
078        for (Glyph g: sbgn.getMap().getGlyph())
079        {
080            if(g.getCompartmentRef() != null)
081            {
082                Glyph containerCompartment = (Glyph)g.getCompartmentRef();
083                idToCompartmentGlyphs.get(containerCompartment.getId()).getGlyph().add(g);
084                deletedList.add(g);
085            }
086        }
087
088        // Delete the duplicate glyphs, after they are moved to corresponding compartment glyph.
089        for (Glyph g: deletedList)
090        {
091            sbgn.getMap().getGlyph().remove(g);
092        }
093
094        // initialize the map for keeping ports and their owner glyphs
095        // with entries like: <portID, ownerGlyph>
096        initPortIdToGlyphMap(sbgn.getMap().getGlyph());
097
098        //Remove ports from source and target field of ports
099        //replace them with owner glyphs of these ports
100        removePortsFromArcs(sbgn.getMap().getArc());
101
102        // Assign logical operator and Process nodes to compartment
103        assignProcessAndLogicOpNodesToCompartment(sbgn);
104
105        // Create Vnodes for ChiLay layout component
106        createVNodes(root, sbgn.getMap().getGlyph());
107
108        for (VNode vNode: this.root.children)
109        {
110            this.createLNode(vNode, null, this.layout);
111        }
112
113        // Create LEdges for ChiLay layout component
114        createLEdges(sbgn.getMap().getArc(), this.layout);
115        graphMgr.updateBounds();
116
117        // Apply layout
118        this.layout.runLayout();
119        graphMgr.updateBounds();
120
121        // Here if any SbgnProcessNode node is returned from SBGNPD Layout
122        // this means that we will have two additional port info. We should
123        // add this information to libSbgn objects
124        for(Object lNode: this.layout.getAllNodes())
125        {
126            if( (LNode)lNode instanceof SbgnProcessNode)
127            {
128                //Set geometry of corresponding node
129                VNode vNode = layoutToView.get(((SbgnProcessNode) lNode).label);
130                Bbox tempBbox = vNode.glyph.getBbox();
131                tempBbox.setX((float)(((SbgnProcessNode) lNode).getLeft()));
132                tempBbox.setY((float) (((SbgnProcessNode) lNode).getTop()));
133                vNode.glyph.setBbox(tempBbox);
134
135                //Created port objects in layout level
136                SbgnPDNode inputLPort = ((SbgnProcessNode) lNode).getInputPort();
137                SbgnPDNode outputLPort = ((SbgnProcessNode) lNode).getOutputPort();
138
139                // New port objects
140                Port inputPort = new Port();
141                Port outputPort = new Port();
142
143                // Set port attributes
144                inputPort.setX((float) (inputLPort.getCenterX()));
145                inputPort.setY((float)(inputLPort.getCenterY()));
146                inputPort.setId(inputLPort.label);
147
148                outputPort.setX((float)(outputLPort.getCenterX()));
149                outputPort.setY((float) (outputLPort.getCenterY()));
150                outputPort.setId((outputLPort.label));
151
152                //Clear existing ports !
153                vNode.glyph.getPort().clear();
154                //Connect existing arcs to newly created ports
155                //These ports are created by ChiLay and SBGNPD Layout
156                connectArcToPort(inputLPort, inputPort);
157                connectArcToPort(outputLPort, outputPort);
158
159                //Add ports to the corresponding glyph
160                vNode.glyph.getPort().add(inputPort);
161                vNode.glyph.getPort().add(outputPort);
162            }
163        }
164
165        // Update the bounds
166        for (VNode vNode: this.root.children)
167        {
168            updateCompoundBounds(vNode.glyph, vNode.glyph.getGlyph());
169        }
170
171        // Clear inside of the compartmentGlyphs
172        for (Glyph compGlyph: idToCompartmentGlyphs.values())
173        {
174            //Again add the members of compartments
175            for(Glyph memberGlyph:compGlyph.getGlyph() )
176            {
177                sbgn.getMap().getGlyph().add(memberGlyph);
178            }
179            compGlyph.getGlyph().clear();
180        }
181
182        return sbgn;
183    }
184
185    /**
186     * This method connects the existing arcs to the newly created
187     * ports which are created by ChiLay and SBGNPD Layout.
188     * @param lPort l level port object.
189     * @param vPort v level port object.
190     * */
191    public void connectArcToPort(SbgnPDNode lPort, Port vPort)
192    {
193        //Iterate over the edges of l level port
194        for  (Object e: (lPort.getEdges()))
195        {
196            //Ignore rigid edges
197            if(((LEdge)e).type.equals("rigid edge"))
198                continue;
199
200            //Determine the if vPort is source or target
201            Arc arc = idToArcs.get(((LEdge)e).label);
202            if( lPort.label.equals(((LEdge)e).getSource().label ))
203            {
204                arc.setSource(vPort);
205            }
206            else if ( lPort.label.equals(((LEdge)e).getTarget().label ) )
207            {
208                arc.setTarget(vPort);
209            }
210        }
211    }
212
213    /**
214     * This method finds process nodes and logical operator nodes in sbgn map and assigns them to a compartment by using majority rule.
215     * @param sbgn Given Sbgn map.
216     * */
217    public void assignProcessAndLogicOpNodesToCompartment(Sbgn sbgn)
218    {
219        // Create a hashmap for keeping a node( generally logical operators and process nodes ) and its neighbours.
220        // TreeMap value of the hash map keeps track of compartment nodes that includes neighbours of the node by String id and
221        // Integer value holds the number of occurences of that compartment among the neighbours of the node as parent.
222        HashMap <String, HashMap<String,Integer>>  nodetoNeighbours = new HashMap<String, HashMap<String,Integer>>();
223        List<Glyph> glyphList = sbgn.getMap().getGlyph();
224        List<Arc>   arcList   = sbgn.getMap().getArc();
225
226        // Keeps track of process and logical operator nodes that will be assigned to a compartment.
227        ArrayList<Glyph> targetNodes = new ArrayList<Glyph>();
228
229        //Iterate over glyphs of sbgn map
230        for(Glyph glyph: glyphList)
231        {
232            // Here logical operator nodes and process nodes are interested !
233            if(glyph.getClazz().equals("process") ||glyph.getClazz().equals("omitted process")||glyph.getClazz().equals( "uncertain process") || glyph.getClazz().equals("phenotype")||
234                    glyph.getClazz().equals("association") || glyph.getClazz().equals("dissociation") ||glyph.getClazz().equals("and")||glyph.getClazz().equals("or")||glyph.getClazz().equals("not"))
235            {
236                // Add a new value to hash map and also store the node as target node
237                String processGlyphID = glyph.getId();
238                String rootID = "root";
239                nodetoNeighbours.put(processGlyphID, new HashMap<String, Integer>());
240                targetNodes.add(glyph);
241
242                // Iterate over arc list
243                for(Arc arc: arcList)
244                {
245                    Glyph target = null;
246                    Glyph source = null;
247
248                    // If source and target of node is port find its owner glyph ! else just assign it.
249                    if(arc.getSource() instanceof Port)
250                        source = portIDToOwnerGlyph.get(((Port)arc.getSource()).getId());
251                    else
252                        source = (Glyph)arc.getSource();
253
254                    if(arc.getTarget() instanceof Port)
255                        target = portIDToOwnerGlyph.get(((Port)arc.getTarget()).getId());
256                    else
257                        target = (Glyph)arc.getTarget();
258
259                    // If source of any arc is our node, then target must be neighbour of this node !
260                    if(source.getId().equals(processGlyphID))
261                    {
262                        populateCompartmentOccurencesMap(target, nodetoNeighbours.get(processGlyphID));
263                    }
264                    // same as target part !!
265                    else if(target.getId().equals(processGlyphID))
266                    {
267                        populateCompartmentOccurencesMap(source, nodetoNeighbours.get(processGlyphID));
268                    }
269                }
270            }
271        }
272
273        //Finally assign nodes to compartments by majority rule
274        for(Glyph glyph: targetNodes)
275        {
276            String id = glyph.getId();
277            HashMap<String, Integer> compartmentsOfTargetNode = nodetoNeighbours.get(id);
278
279            // Finally sort the hashmap to obtain the compartment that includes majority of the neighbours of the targetNode 
280            List<Map.Entry<String,Integer>> entries = new LinkedList<Map.Entry<String,Integer>>(compartmentsOfTargetNode.entrySet());
281            Collections.sort(entries, new Comparator<Map.Entry<String,Integer>>() {
282
283                @Override
284                public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2)
285                {
286                    return -(o1.getValue().compareTo(o2.getValue()));
287                }
288            });
289
290            if(entries.size() > 0)
291            {
292                // if the process belongs to root do not make any changes
293                if(entries.get(0).getKey().equals("root"))
294                    continue;
295
296                Glyph compartment = idToCompartmentGlyphs.get(entries.get(0).getKey());
297                //Set compartmentRef of process glyph also!
298                glyph.setCompartmentRef(compartment);
299                compartment.getGlyph().add(glyph);
300
301                //Remove it from sbgn also
302                sbgn.getMap().getGlyph().remove(glyph);
303            }
304        }
305    }
306
307    /**
308     * Updates a hashmap by incrementing the number of nodes in the compartment glyph that includes targetGlyph.
309     * It is assumed that given hashmap includes compartment ids' as keys and number of nodes as values.This method 
310     * is an utility method that will be used to populate a hashmap while determining the compartment node of a process
311     * node by majority rule.
312     *
313     * @param targetGlyph glyph whose occurence will be updated in the given hashmap.
314     * @param compartmentIDandOccurenceMap  Map that references number of nodes in a compartment by compartment ids .
315     * */
316    public void populateCompartmentOccurencesMap(Glyph targetGlyph,  HashMap<String, Integer> compartmentIDandOccurenceMap)
317    {
318        String rootID = "root";
319
320        // if compartment ref of targetGlyph node is not null, increment its occurence by 1
321        if(targetGlyph.getCompartmentRef() != null)
322        {
323            Glyph  containerCompartment = (Glyph)targetGlyph.getCompartmentRef();
324            String compartmentID = containerCompartment.getId();
325            Integer compartmentOccurrenceValue = compartmentIDandOccurenceMap.get(compartmentID);
326
327            if( compartmentOccurrenceValue != null)
328            {
329                compartmentIDandOccurenceMap.put(compartmentID, compartmentOccurrenceValue + 1);
330            }
331            else
332                compartmentIDandOccurenceMap.put(compartmentID, 1);
333        }
334        // else targetGlyph is in root graph so increment root graphs counter value by 1
335        else
336        {
337            Integer compartmentOccurrenceValue = compartmentIDandOccurenceMap.get(rootID);
338
339            if( compartmentOccurrenceValue != null)
340            {
341                compartmentIDandOccurenceMap.put(rootID, compartmentOccurrenceValue + 1);
342            }
343            else
344                compartmentIDandOccurenceMap.put(rootID, 1);
345        }
346    }
347
348    /**
349     * Updates bounds of a compound node ( i.e. complex glyph ) from its children .
350     * @param parent compound glyph.
351     * @param childGlyphs related children of parent .
352     * */
353    public void updateCompoundBounds(Glyph parent,List<Glyph> childGlyphs)
354    {
355        float PAD = (float) 2.0;
356        float minX = Float.MAX_VALUE; float minY = Float.MAX_VALUE;
357        float maxX = Float.MIN_VALUE; float maxY = Float.MIN_VALUE;
358
359        for (Glyph tmpGlyph:childGlyphs)
360        {
361            if(!tmpGlyph.getClazz().equals("unit of information") && !tmpGlyph.getClazz().equals("state variable") )
362            {
363                if(tmpGlyph.getGlyph().size() > 0)
364                    updateCompoundBounds(tmpGlyph, tmpGlyph.getGlyph());
365
366                float w = tmpGlyph.getBbox().getW();
367                float h = tmpGlyph.getBbox().getH();
368
369                // Verify MIN and MAX x/y again:
370                minX = Math.min(minX, (tmpGlyph.getBbox().getX()));
371                minY = Math.min(minY, (tmpGlyph.getBbox().getY()));
372                maxX = Math.max(maxX, (tmpGlyph.getBbox().getX())+w);
373                maxY = Math.max(maxY, (tmpGlyph.getBbox().getY())+h);
374
375                if (minX == Float.MAX_VALUE) minX = 0;
376                if (minY == Float.MAX_VALUE) minY = 0;
377                if (maxX == Float.MIN_VALUE) maxX = 0;
378                if (maxY == Float.MIN_VALUE) maxY = 0;
379
380                parent.getBbox().setX(minX - PAD);
381                parent.getBbox().setY(minY - PAD);
382                parent.getBbox().setW(maxX -  parent.getBbox().getX() + PAD);
383                parent.getBbox().setH(maxY -  parent.getBbox().getY() + PAD);
384            }
385        }
386    }
387
388    /**
389     * Recursively creates VNodes from Glyphs of Sbgn.
390     *
391     * @param parent Parent of the glyphs that are passed as second arguement.
392     * @param glyphs Glyphs that are child of parent which is passed as first arguement.
393     *
394     * */
395    public void createVNodes(VCompound parent,List<Glyph> glyphs)
396    {
397        for(Glyph glyph: glyphs )
398        {
399            if (!glyph.getClazz().equals("state variable") && !glyph.getClazz().equals("unit of information")  )
400            {
401                if(glyph.getClazz().equals("process"))
402                {
403                    VCompound v = new VCompound(glyph);
404                }
405
406                if(!this.isChildless(glyph))
407                {
408                    VCompound v = new VCompound(glyph);
409
410                    idToGLyph.put(glyph.getId(), glyph);
411                    glyphToVNode.put(glyph, v);
412                    parent.children.add(v);
413                    createVNodes(v, glyph.getGlyph());
414                }
415
416                else
417                {
418                    VNode v = new VNode(glyph);
419                    idToGLyph.put(glyph.getId(), glyph);
420                    glyphToVNode.put(glyph, v);
421                    parent.children.add(v);
422                }
423            }
424        }
425    }
426
427    /**
428     * Creates LNodes from Arcs of Sbgn and adds it to the passed layout object.
429     *
430     * @param arcs List of arc objects from which the LEdges will be constructed for ChiLay Layout component.
431     * @param layout layout object to which the created LEdges added.
432     *
433     * */
434    public void createLEdges(List<Arc> arcs, Layout layout)
435    {
436        for(Arc arc: arcs )
437        {
438            LEdge lEdge = layout.newEdge(null);
439            lEdge.type = arc.getClazz();
440            lEdge.label = arc.getId();
441            LNode sourceLNode = this.viewToLayout.get(glyphToVNode.get(arc.getSource()));
442            LNode targetLNode = this.viewToLayout.get(glyphToVNode.get(arc.getTarget()));
443            idToArcs.put(arc.getId(), arc);
444
445            // Add edge to the layout
446            this.layout.getGraphManager().add(lEdge, sourceLNode, targetLNode);
447        }
448    }
449
450    /**
451     * Helper function for creating LNode objects from VNode objects and adds them to the given layout.
452     *
453     * @param vNode  VNode object from which a corresponding LNode object will be created.
454     * @param parent parent of vNode, if not null vNode will be added to layout as child node.
455     * @param layout layout object to which the created LNodes added.
456     * */
457
458    public void createLNode(VNode vNode,VNode parent,Layout layout)
459    {
460        LNode lNode = ((SbgnPDLayout)layout).newNode(vNode);
461        lNode.type  = vNode.glyph.getClazz();
462        lNode.label = vNode.glyph.getId();
463        LGraph rootLGraph = layout.getGraphManager().getRoot();
464
465        //Add corresponding nodes to corresponding maps
466        this.viewToLayout.put(vNode, lNode);
467        layoutToView.put(lNode.label,vNode);
468
469        // if the vNode has a parent, add the lNode as a child of the parent l-node.
470        // otherwise, add the node to the root graph.
471        if (parent != null)
472        {
473            LNode parentLNode = this.viewToLayout.get(parent);
474            parentLNode.getChild().add(lNode);
475        }
476
477        else
478        {
479            rootLGraph.add(lNode);
480        }
481
482        lNode.setLocation(vNode.glyph.getBbox().getX(),vNode.glyph.getBbox().getY());
483
484        if (vNode instanceof VCompound)
485        {
486            VCompound vCompound = (VCompound) vNode;
487            // add new LGraph to the graph manager for the compound node
488            layout.getGraphManager().add(layout.newGraph(null), lNode);
489            // for each VNode in the node set create an LNode
490            for (VNode vChildNode: vCompound.getChildren())
491            {
492                this.createLNode(vChildNode, vCompound, layout);
493            }
494        }
495
496        else
497        {
498            lNode.setWidth(vNode.glyph.getBbox().getW());
499            lNode.setHeight(vNode.glyph.getBbox().getH());
500        }
501    }
502
503    /**
504     * This method recursively set compartmentRef fields of members of any complex glyphs
505     * as same as complex's compartmentRef
506     *
507     * @param glyph target glyph whose compartmentRef(compartment parameter) will be set.
508     * @param compartment compartmentRef value that will be set.
509     * */
510    public void setCompartmentRefForComplexMembers(Glyph glyph, Glyph compartment)
511    {
512        glyph.setCompartmentRef(compartment);
513        if(glyph.getCompartmentRef() != null && glyph.getGlyph().size() > 0)
514        {
515            for(Glyph g: glyph.getGlyph() )
516            {
517                setCompartmentRefForComplexMembers(g, compartment);
518            }
519        }
520    }
521
522    /**
523     * This method replaces ports of arc objects with their owners.
524     * @param arcs Arc list of SBGN model
525     * */
526    public void removePortsFromArcs(List<Arc> arcs)
527    {
528        for(Arc arc: arcs )
529        {
530            // If source is port, first clear port indicators else retrieve it from hashmaps
531            if (arc.getSource() instanceof Port )
532            {
533                Glyph source = portIDToOwnerGlyph.get(((Port)arc.getSource()).getId());
534                arc.setSource(source);
535            }
536
537            // If target is port, first clear port indicators else retrieve it from hashmaps
538            if (arc.getTarget() instanceof Port)
539            {
540                Glyph target = portIDToOwnerGlyph.get(((Port)arc.getTarget()).getId());
541                arc.setTarget(target);
542            }
543        }
544    }
545
546    /**
547     * This method initializes map for glyphs and their respective ports.
548     * @param glyphs Glyph list of SBGN model
549     * */
550    public void initPortIdToGlyphMap(List<Glyph> glyphs)
551    {
552        for(Glyph glyph: glyphs)
553        {
554            for(Port p: glyph.getPort())
555            {
556                portIDToOwnerGlyph.put(p.getId(), glyph );
557            }
558            if(glyph.getGlyph().size() > 0)
559                initPortIdToGlyphMap(glyph.getGlyph());
560        }
561    }
562
563    /**
564     * Returns true if a glyph includes child glyphs (state and info glyphs are out of count!)
565     * @param targetGlyph target glyph that will be queried.
566     * @return true/false
567     * */
568    public boolean isChildless(Glyph targetGlyph)
569    {
570        boolean checker = true;
571        for(Glyph glyph: targetGlyph.getGlyph() )
572        {
573            if ( !glyph.getClazz().equals("state variable") && !glyph.getClazz().equals("unit of information")  )
574            {
575                checker = false;
576                break;
577            }
578        }
579        return checker;
580    }
581}