package com.github.lindenb.jvarkit.tools.treepack; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.io.InputStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import htsjdk.samtools.util.CloserUtil; import com.beust.jcommander.Parameter; import com.github.lindenb.jvarkit.io.IOUtils; import com.github.lindenb.jvarkit.util.Hershey; import com.github.lindenb.jvarkit.util.jcommander.Launcher; import com.github.lindenb.jvarkit.util.log.Logger; public class TreePackApp extends Launcher { private static final Logger LOG = Logger.build(TreePackApp.class).make(); @Parameter(names="-x",description="{width}x{height} dimension of the output rectangle") protected Rectangle viewRect=new Rectangle(1000,1000); protected final String SVG="http://www.w3.org/2000/svg"; protected final String XLINK="http://www.w3.org/1999/xlink"; protected final String JVARKIT_NS="https://github.com/lindenb/jvarkit"; protected Node root=null; protected Hershey hershey=new Hershey(); private class Node implements TreePack { private Node parent=null; private Rectangle2D bounds=new Rectangle2D.Double(-10,-10,10,10); private String label=""; private Map<String,Node> children=null; private Double weight=null; protected Node(Node parent) { this.parent=parent; } public final boolean isLeaf() { return children==null || children.isEmpty(); } protected String convertWeightToString() { return TreePackApp.this.convertWeightToString(getWeight()); } public String getPath() { StringBuilder b=new StringBuilder(); Node curr=this; while(curr!=null) { if(b.length()!=0) b.insert(0,"/"); b.insert(0, curr.getLabel()); curr=curr.parent; } return b.toString(); } public String getLabel() { return this.label; } @Override public void setBounds(Rectangle2D bounds) { this.bounds=bounds; } public Rectangle2D getInsetBounds() { final double ratio=0.9; final Rectangle2D r0=getBounds(); Rectangle2D.Double r1=new Rectangle2D.Double(); r1.width=r0.getWidth()*ratio; r1.x=r0.getX()+(r0.getWidth()-r1.width)/2; r1.height=r0.getHeight()*ratio; r1.y=r0.getY()+(r0.getHeight()-r1.height)/2; return r1; } @Override public Rectangle2D getBounds() { return this.bounds; } public double getWeight() { if(isLeaf()) { return this.weight==null?1.0:this.weight; } else { double N=0; for(Node c:this.children.values()) { if(c.getWeight()<=0) throw new IllegalStateException(getPath()); N+=c.getWeight(); } return N; } } protected Rectangle2D getTitleFrame() { Rectangle2D r=getBounds(); Rectangle2D.Double frame= new Rectangle2D.Double(); frame.height=r.getHeight()*0.1; frame.y=r.getY(); frame.width=r.getWidth(); frame.x=r.getX(); return frame; } protected Rectangle2D getChildrenFrame() { Rectangle2D r=getBounds(); Rectangle2D.Double frame= new Rectangle2D.Double(); frame.height=r.getHeight()*0.9;//yes again after frame.y=r.getMaxY()-frame.height; frame.height=r.getHeight()*0.85;//yes again frame.width=r.getWidth()*0.95; frame.x=r.getX()+(r.getWidth()-frame.width)/2.0; return frame; } public void layout(TreePacker packer) { if(getWeight()<=0 || isLeaf()) return ; List<TreePack> L=new ArrayList<TreePack>(children.values()); packer.layout(L, getChildrenFrame() ); for(Node c:children.values()) { c.layout(packer); } } public int getDepth() { int d=0; Node p=this; while(p.parent!=null) { p=p.parent; ++d; } return d; } public void svg(XMLStreamWriter w) throws XMLStreamException { if(getWeight()<=0) { LOG.info("weight < 0 for "+getPath()); return ; } final Rectangle2D bounds=this.getBounds(); Rectangle2D insets=this.getInsetBounds(); Rectangle2D frameUsed=bounds; if(bounds.getWidth()<=1 || bounds.getHeight()<=1) { return; } w.writeStartElement("svg","g",SVG); w.writeAttribute("title",getPath()+"="+convertWeightToString()); w.writeAttribute("jvarkit",JVARKIT_NS,"path",getPath()); w.writeAttribute("jvarkit",JVARKIT_NS,"weight",convertWeightToString()); if(!isLeaf()) { w.writeAttribute("jvarkit",JVARKIT_NS,"count",""+ this.children.size()); } w.writeEmptyElement("svg", "rect", SVG); w.writeAttribute("class", "r"+(getDepth()%2)); w.writeAttribute("x",String.valueOf(frameUsed.getX())); w.writeAttribute("y",String.valueOf(frameUsed.getY())); w.writeAttribute("width",String.valueOf(frameUsed.getWidth())); w.writeAttribute("height",String.valueOf(frameUsed.getHeight())); if(!isLeaf()) { w.writeEmptyElement("svg", "path", SVG); w.writeAttribute("d", hershey.svgPath(getLabel()+"="+convertWeightToString(), this.getTitleFrame()) ); } if(isLeaf()) { Rectangle2D f_up=new Rectangle2D.Double( insets.getX(),insets.getY(), insets.getWidth(),insets.getHeight()/2.0 ); w.writeEmptyElement("svg", "path", SVG); w.writeAttribute("d", hershey.svgPath(getLabel(),f_up) ); w.writeAttribute("class","lbla"+(getDepth()%2)); Rectangle2D f_down=new Rectangle2D.Double( insets.getX(),insets.getCenterY(), insets.getWidth(),insets.getHeight()/2.0 ); w.writeEmptyElement("svg", "path", SVG); w.writeAttribute("d", hershey.svgPath(convertWeightToString(),f_down) ); w.writeAttribute("class","lblb"+(getDepth()%2)); } else { for(Node child:children.values()) { child.svg(w); } } w.writeEndElement();//g } } protected String getCascadingStylesheet() { return "svg {fill:none;stroke:black;stroke-width:0.5px;}\n"+ ".r0 {fill:none;stroke:black;stroke-width:0.5px;}\n"+ ".r1 {fill:none;stroke:black;stroke-width:0.5px;}\n"+ ".lbla0 {stroke:black;stroke-width:0.3px;}\n"+ ".lblb0 {stroke:red;stroke-width:0.3px;}\n"+ ".lbla1 {stroke:gray;stroke-width:0.3px;}\n"+ ".lblb1 {stroke:red;stroke-width:0.3px;}\n"+ "" ; } protected void svg() throws XMLStreamException { LOG.info("writing svg"); XMLOutputFactory xmlfactory= XMLOutputFactory.newInstance(); XMLStreamWriter w= xmlfactory.createXMLStreamWriter(System.out,"UTF-8"); w.writeStartDocument("UTF-8","1.0"); w.writeStartElement("svg","svg",SVG); w.writeAttribute("xmlns", XMLConstants.XML_NS_URI, "svg", SVG); w.writeAttribute("xmlns", XMLConstants.XML_NS_URI, "xlink", XLINK); w.writeAttribute("xmlns", XMLConstants.XML_NS_URI, "jvarkit", JVARKIT_NS); w.writeAttribute("width",String.valueOf(this.viewRect.getWidth())); w.writeAttribute("height",String.valueOf(this.viewRect.getHeight())); w.writeStartElement("svg","defs",SVG); w.writeStartElement("svg","style",SVG); w.writeAttribute("type","text/css"); w.writeCharacters(getCascadingStylesheet()); w.writeEndElement();//svg:style w.writeEndElement();//svg:def w.writeStartElement("svg","title",SVG); w.writeCharacters(getProgramName()); w.writeEndElement(); w.writeComment("Cmd-Line:"+getProgramCommandLine()); w.writeComment("Version "+getVersion()); root.svg(w); w.writeEndElement();//svg w.writeEndDocument(); w.flush(); } protected TreePackApp() { } protected String convertWeightToString(double weight) { NumberFormat format=NumberFormat.getInstance(); return format.format((long)weight); } protected String intervalToString(int value,int step) { value=((int)(value/(double)step)); return String.valueOf((int)(value)*step+"-"+((value+1)*step)); } private void layout() { LOG.info("layout"); this.root.setBounds(new Rectangle2D.Double(0, 0, this.viewRect.getWidth(), this.viewRect.getHeight())); TreePacker packer=new TreePacker(); this.root.layout(packer); } private void skip(XMLEventReader r) throws XMLStreamException { while(r.hasNext()) { XMLEvent evt=r.nextEvent(); if(evt.isStartElement()) { skip(r); } else if(evt.isEndElement()) { return; } } throw new XMLStreamException(); } private Node parseNode(XMLEventReader r,StartElement E,Node parent) throws XMLStreamException { Node node=new Node(parent); Attribute att=E.getAttributeByName(new QName("label")); node.label=(att!=null?att.getValue():""); while(r.hasNext()) { XMLEvent evt=r.nextEvent(); if(evt.isStartElement()) { QName qName=evt.asStartElement().getName(); if(qName.getLocalPart().equals("node")) { Node child=parseNode(r,evt.asStartElement(),node); if(node.children==null) node.children=new HashMap<String,Node>(); node.children.put(node.label,child); } else { skip(r); } } else if(evt.isEndElement()) { if(node.children==null || node.children.isEmpty()) { att=E.getAttributeByName(new QName("weight")); if(att==null) { node.weight=1.0; } else { node.weight=Double.parseDouble(att.getValue()); if(node.weight<=0) throw new XMLStreamException("bad @weight:"+node.weight, E.getLocation()); } } return node; } } throw new IllegalStateException(); } private Node scan(InputStream in) throws XMLStreamException { Node n=null; XMLInputFactory xif=XMLInputFactory.newFactory(); XMLEventReader r=xif.createXMLEventReader(in); while(r.hasNext()) { XMLEvent evt=r.nextEvent(); if(evt.isStartElement()) { QName qName=evt.asStartElement().getName(); if(qName.getLocalPart().equals("node")) { n=parseNode(r,evt.asStartElement(),null); } } } r.close(); return n; } @Override public int doWork(List<String> args) { if(viewRect.x<=0 ||this.viewRect.y<=0 ) { LOG.error("bad viewRect "+viewRect); return -1; } try { if(args.isEmpty()) { LOG.info("Reading from stdin"); this.root=scan(stdin()); } else if(args.size()==1) { String filename=args.get(0); LOG.info("Reading from "+filename); InputStream in=IOUtils.openURIForReading(filename); this.root=scan(in); CloserUtil.close(in); } else { LOG.error("Illegal number of arguments."); return -1; } layout(); return 0; } catch(Exception err) { LOG.error(err); return -1; } finally { } } }