/* ant jeter && cat jeter2.bed | java -jar dist/jeter.jar -u src/main/resources/img/World_V2.0.svg -o ~/jeter.jpg -R /commun/data/pubdb/broadinstitute.org/bundle/1.5/b37/human_g1k_v37.fasta && eog ~/jeter.jpg gunzip -c ~/jeter.gz | gawk -f countries.awk | sed 's/^chr//' | grep -v '_' > jeter2.bed */ package com.github.lindenb.jvarkit.tools.circular; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.regex.Pattern; import javax.imageio.ImageIO; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import htsjdk.tribble.readers.LineIterator; import htsjdk.tribble.readers.LineIteratorImpl; import htsjdk.tribble.readers.SynchronousLineReader; import htsjdk.variant.utils.SAMSequenceDictionaryExtractor; import htsjdk.samtools.SAMSequenceRecord; import htsjdk.samtools.util.CloserUtil; import com.beust.jcommander.Parameter; import com.github.lindenb.jvarkit.io.IOUtils; import com.github.lindenb.jvarkit.util.Counter; import com.github.lindenb.jvarkit.util.bio.circular.CircularContext; import com.github.lindenb.jvarkit.util.jcommander.Launcher; import com.github.lindenb.jvarkit.util.jcommander.Program; import com.github.lindenb.jvarkit.util.log.Logger; import com.github.lindenb.semontology.Term; /** BEGIN_DOC ## Example ``` head map.bed 1 141535 143008 taiwan 1 564463 570304 china 1 564463 570304 china 1 564463 564813 canada 1 564510 569779 estonia 1 564510 569779 estonia 1 564510 569170 germany 1 564633 569139 italy 1 564660 565200 iran 1 564733 569130 brazil 1 564776 569431 mexico ``` ```bash $ cat map.bed |\ java -jar dist/worldmapgenome.jar \ -u World_V2.0.svg \ -w 2000 -o ~/ouput.jpg \ -R human_g1k_v37.fasta ``` ![worldmapgenome](https://pbs.twimg.com/media/BfGE0X4CMAAfRAR.jpg) ## See also * https://twitter.com/yokofakun/status/428269474869817344 * https://twitter.com/GenomeBrowser/status/426398651103997953 END_DOC */ @Program(name="worldmapgenome", description="Genome/Map of the world. Input is a BED file: chrom/start/end/country.", keywords="gis", terms=Term.ID_0000017 ) public class WorldMapGenome extends Launcher { private static final Logger LOG = Logger.build(WorldMapGenome.class).make(); @Parameter(names="-R",description=" (ref) fasta reference file indexed with samtools. Required.") File faidx=null; @Parameter(names="-w",description=" (int: size) square size") private int squareSize=1000; @Parameter(names="-T",description=" just list countries and exit.",help=true) private boolean listCountries=false; @Parameter(names="-o",description=" (file.jpg) ouput file. Required") private File fileout=null; private CircularContext context=null; /* ouput size */ private Dimension viewRect=new Dimension(1000, 1000); private Map<String,Shape> country2shape=new HashMap<String,Shape>(); /* SVG map location */ @Parameter(names="-u",description=" (uri) URL world SVG map.") private String svgMapUri="http://upload.wikimedia.org/wikipedia/commons/7/76/World_V2.0.svg"; /* number of time we saw each country */ private Counter<String> seen=new Counter<String>(); private WorldMapGenome() { } private void loadWorld() throws IOException,XMLStreamException { Source source; LOG.warning("openingg "+svgMapUri); if(IOUtils.isRemoteURI(svgMapUri)) { source=new StreamSource(svgMapUri); } else { source=new StreamSource(new File(svgMapUri)); } XMLInputFactory xif=XMLInputFactory.newFactory(); XMLEventReader xef=xif.createXMLEventReader(source); while(xef.hasNext()) { XMLEvent evt=xef.nextEvent(); if(!evt.isStartElement()) { continue; } StartElement E=evt.asStartElement(); String localName=E.getName().getLocalPart(); if(!localName.equals("path")) continue; Attribute att=E.getAttributeByName(new QName("id")); if(att==null) continue; String country=att.getValue().toLowerCase().replaceAll("[ ]+", ""); att=E.getAttributeByName(new QName("d")); if(att==null) continue; GeneralPath path=null; char op='\0'; Scanner scanner=new Scanner(att.getValue().replaceAll("[ \t\n\r,]+", " ")); path=new GeneralPath(); while(scanner.hasNext()) { if(op=='\0') { op=scanner.next().charAt(0); } switch(op) { case 'M': path.moveTo( scanner.nextDouble(), scanner.nextDouble() ); break; case 'C': path.curveTo( scanner.nextDouble(), scanner.nextDouble(), scanner.nextDouble(), scanner.nextDouble(), scanner.nextDouble(), scanner.nextDouble() ); break; case 'Z': { path.closePath(); break; } default:throw new IOException("bad operator "+op); } if(scanner.hasNext("[MCZ]")) { op=scanner.next().charAt(0); } } scanner.close(); this.country2shape.put(country, scaleWorld(path)); } xef.close(); } private Shape scaleWorld(Shape shape) { Dimension svgDim=new Dimension(800,400); double diag=Math.sqrt(Math.pow(svgDim.width/2, 2)+Math.pow(svgDim.height/2, 2)); double ratio=getRadiusInt()/diag; //move to center of figure AffineTransform tr=AffineTransform.getTranslateInstance(-svgDim.width/2, -svgDim.height/2); shape=tr.createTransformedShape(shape); tr=AffineTransform.getScaleInstance(ratio,ratio); shape=tr.createTransformedShape(shape); tr=AffineTransform.getTranslateInstance(viewRect.width/2,viewRect.height/2); shape=tr.createTransformedShape(shape); return shape; } @Override public int doWork(List<String> args) { try { viewRect.width=squareSize; viewRect.height=squareSize; loadWorld(); if(listCountries) { for(String country:this.country2shape.keySet()) { System.out.println(country); } return 0; } if(faidx==null) { LOG.error("undefined reference file"); return -1; } if(fileout==null) { LOG.error("undefined output file"); return -1; } LOG.info("loading "+faidx); this.context=new CircularContext(SAMSequenceDictionaryExtractor.extractDictionary(faidx)); this.context.setCenter(viewRect.width/2,viewRect.height/2); BufferedImage offscreen=new BufferedImage( viewRect.width, viewRect.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g=(Graphics2D)offscreen.getGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); if(args.isEmpty()) { LOG.info("Reading from stdin"); scan(g,stdin()); } else { for(String filename :args) { LOG.info("Reading from "+filename); InputStream in=IOUtils.openURIForReading(filename); scan(g,in); CloserUtil.close(in); } } g.dispose(); BufferedImage background=new BufferedImage( viewRect.width, viewRect.height, BufferedImage.TYPE_INT_RGB); g=(Graphics2D)background.getGraphics(); //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); //paint background paintGenome(g); paintWorld(g); g.drawImage(offscreen, 0, 0, null); g.dispose(); ImageIO.write(background, "jpg", fileout); return 0; } catch(Exception err) { LOG.error(err); return -1; } finally { } } private int getRadiusInt() { return getRadiusExt()-20; } private int getRadiusExt() { return Math.min(viewRect.height, viewRect.width)/2-20; } private void paintWorld(Graphics2D g) { for(String country:this.country2shape.keySet()) { Shape shape=country2shape.get(country); float color=0; if(seen.count(country)!=0) { color=1f-(float)(Math.log(seen.count(country))/Math.log((double)seen.getTotal())); } g.setColor(seen.count(country)==0?Color.WHITE:new Color(color,0f,0f)); g.fill(shape); } for(String country:this.country2shape.keySet()) { Shape shape=country2shape.get(country); g.setColor(Color.BLACK); g.draw(shape); } } private void paintGenome(Graphics2D g) { g.setColor(Color.WHITE); g.fillRect(0, 0, viewRect.width, viewRect.height); g.setColor(new Color(0.9f,0.9f,1f)); g.fill(new Ellipse2D.Double(context.getCenterX()-getRadiusInt(),context.getCenterY()-getRadiusInt(),getRadiusInt()*2, getRadiusInt()*2)); for(int i=0;i< this.context.getDictionary().getSequences().size();++i) { SAMSequenceRecord rec=this.context.getDictionary().getSequence(i); Arc2D outer=context.getArc(rec, 0, rec.getSequenceLength(), getRadiusExt(), Arc2D.PIE); if(outer.getAngleExtent()==0) continue; Area area=new Area(outer); Ellipse2D.Double ed=new Ellipse2D.Double( context.getCenter().getX()-getRadiusInt(), context.getCenter().getY()-getRadiusInt(), getRadiusInt()*2, getRadiusInt()*2 ); area.subtract(new Area(ed)); g.setColor(i%2==0?Color.LIGHT_GRAY:Color.WHITE); g.fill(area); g.setColor(Color.BLACK); g.draw(area); if((rec.getSequenceLength()/(double)this.context.getDictionary().getReferenceLength())<0.01) continue; String title=rec.getSequenceName(); double midangle=context.convertPositionToRadian( rec, rec.getSequenceLength()/2 ); AffineTransform old=g.getTransform(); AffineTransform tr=new AffineTransform(old); g.translate(context.getCenterX(),context.getCenterY()); g.rotate(midangle); g.translate(getRadiusExt(),0); g.drawString( title, 0, 0 ); g.setTransform(tr); g.setTransform(old); } } private void scan(Graphics2D g,InputStream input) throws IOException { Set<String> unknownC=new HashSet<String>(); Pattern tab=Pattern.compile("[\t]"); LineIterator in=new LineIteratorImpl(new SynchronousLineReader(input)); while(in.hasNext()) { String line=in.next(); String tokens[]=tab.split(line,5); if(tokens.length<4) { LOG.warning("Ignoring "+line); continue; } SAMSequenceRecord rec=this.context.getDictionary().getSequence(tokens[0]); if(rec==null) { LOG.warning("unknown chromosome "+tokens[0]); continue; } String country=tokens[3].toLowerCase().replaceAll("[ ]", ""); Shape shape=this.country2shape.get(country); if(shape==null) { if(!unknownC.contains(country)) { unknownC.add(country); LOG.warning("unknown country "+country); } continue; } seen.incr(country); int midpos=(Integer.parseInt(tokens[1])+Integer.parseInt(tokens[2]))/2; //country center Point2D.Double pt1 =new Point2D.Double(shape.getBounds2D().getCenterX(),shape.getBounds2D().getCenterY()); //circle point Point2D pt3= this.context.convertPositionToPoint(tokens[0],midpos,getRadiusInt()); double angle= this.context.convertPositionToRadian(rec, midpos); double angle2=angle-=Math.PI/10.0; double distance13= context.getCenter().distance(new Point2D.Double( (pt1.getX()+pt3.getX())/2.0, (pt1.getY()+pt3.getY())/2.0 )); //mid point Point2D pt2 =new Point2D.Double( context.getCenterX()+distance13*Math.cos(angle2), context.getCenterX()+distance13*Math.sin(angle2) ); Composite old=g.getComposite(); Stroke olds=g.getStroke(); g.setStroke(new BasicStroke(0.8f)); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.02f)); g.setColor(Color.DARK_GRAY); GeneralPath p=new GeneralPath(); p.moveTo(pt1.getX(), pt1.getY()); p.quadTo(pt2.getX(), pt2.getY(),pt3.getX(), pt3.getY()); p.closePath(); g.draw(p); g.setComposite(old); g.setStroke(olds); } CloserUtil.close(in); } /** * @param args */ public static void main(String[] args) { new WorldMapGenome().instanceMainWithExit(args); } }