/******************************************************************************* * Copyright 2014 Virginia Polytechnic Institute and State University * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package edu.vt.vbi.patric.proteinfamily; import java.awt.Color; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import edu.vt.vbi.patric.msa.SequenceData; public class Newick { private String nexus; private final static int STEM_START = 0; private final static int IN_NAME = 1; private final static int IN_WIDTH = 2; private int tipCount = 0; private Stem[] tips = null; private TipFinder[] nameSearch; private Stem[] stems = null; private double maxDepth = 0.0; private double minWidth = Double.MAX_VALUE; private boolean genomeTips = false; private boolean flushTips = false; private int maxLevel = 0; private final static int distChop = 6; public Newick(String nexus) { this.nexus = nexus; ArrayList<Stem> tipList = new ArrayList<Stem>(); ArrayList<Stem> stemList = new ArrayList<Stem>(); char[] nexChars = nexus.toCharArray(); int nextEnd = nexChars.length; int at = nexus.indexOf('('); Stem branchTop = new Stem(); stemList.add(branchTop); ++at; int state = STEM_START; // set level appropriate for first child of first root int level = 1; int start = at; Stem toFinish = null; while (at < nextEnd) { switch (nexChars[at]) { case '(': // descending to a deeper level // set branch to parent contents past ( branchTop = new Stem(level, branchTop); stemList.add(branchTop); if (state == IN_WIDTH) { // set width of last member at old level toFinish.addWidth(start, at); } toFinish = null; // set to look for tip name in lower level state = STEM_START; // increase level for children of new branchTop ++level; maxLevel = Math.max(level, maxLevel); // do not have anything to finish break; case ':': // about to get branch length if (state == IN_NAME) { // add tip toFinish = new Stem(level, branchTop, start, at); tipList.add(toFinish); stemList.add(toFinish); } // set start to get bounds of branch length start = at + 1; state = IN_WIDTH; break; case ',': if (state == IN_WIDTH) { toFinish.addWidth(start, at); toFinish = null; } // a name might follow a , state = STEM_START; break; case ')': if (state == IN_WIDTH) { toFinish.addWidth(start, at); } // have concluded a level section --level; // next expected is : as in ():branch_length toFinish = branchTop; // adjust branchTop for new level branchTop = branchTop.above; break; default: if (state == STEM_START) { // have the start of a name start = at; state = IN_NAME; } } ++at; } ++maxLevel; tipCount = tipList.size(); tips = new Stem[tipCount]; Iterator<Stem> it = tipList.iterator(); for (int i = tipCount - 1; 0 <= i; i--) { tips[i] = it.next(); } tipList = null; stems = new Stem[stemList.size()]; stemList.toArray(stems); stemList = null; Arrays.sort(stems); // depths at level 0 are known // loop is in increasing level size // deeper levels can compute their depth from their parent for (int i = 1; i < stems.length; i++) { (stems[i]).buildDepth(); } // all depths should now be set // get maxDepth and set display position for tips double tipMid = 1.0; nameSearch = new TipFinder[tips.length]; for (int i = 0; i < tipCount; i++) { Stem nextTip = tips[i]; nameSearch[i] = new TipFinder(i, (tips[i]).tip); maxDepth = Math.max(maxDepth, nextTip.depth); if (0.0 < nextTip.width) { minWidth = Math.min(minWidth, nextTip.width); } nextTip.setDrop(tipMid); tipMid += 1.0; } Arrays.sort(nameSearch); // compute all display positions by using decreasing level // values to propigate bounds to upper levels for (int i = stems.length - 1; 0 <= i; i--) { (stems[i]).sendSpan(); } } public void setTreeType(boolean genomes, boolean flush) { genomeTips = genomes; flushTips = flush; } public String getTipStates() { String result = "locus"; if (genomeTips) { result = "genomes"; } if (flushTips) { result += " flush"; } else { result += " scaled"; } return result; } public int getTipIndex(String name) { int at = Arrays.binarySearch(nameSearch, new TipFinder(0, name)); if (0 <= at) { at = (nameSearch[at]).tipAt; } return at; } public void setGenomeNames(SequenceData[] full) { for (int i = 0; i < full.length; i++) { int at = Arrays.binarySearch(nameSearch, new TipFinder(0, (full[i]).getLocusTag())); if (0 <= at) { (tips[(nameSearch[at]).tipAt]).genome = (full[i]).getTaxonName(); } } } public Graphics2D paint(BufferedImage image) { double maxWide = image.getWidth(); double xExpand = Double.MAX_VALUE; Graphics2D g = image.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.setColor(Color.BLACK); FontMetrics fm = g.getFontMetrics(); int textGap = fm.getHeight() / 4; for (int i = 0; i < tipCount; i++) { Stem nextTip = tips[i]; // get paint width available for double stemRoom = maxWide - nextTip.getTipRoom(fm) - textGap; if (flushTips) { stemRoom /= (1 + nextTip.level); } else { stemRoom /= (minWidth + nextTip.depth); } xExpand = Math.min(stemRoom, xExpand); } if (xExpand < 0) { xExpand = 10.0 / minWidth; } double yExpand = image.getHeight(); yExpand /= (tipCount + 2); if (yExpand < fm.getHeight()) { yExpand = fm.getHeight(); } int textDrop = fm.getHeight() / 2 - fm.getDescent(); for (int i = 0; i < stems.length; i++) { (stems[i]).paint(xExpand, yExpand, textDrop, textGap, g); } return g; } public Dimension getPreferredSize(double minStem, Graphics g) { FontMetrics fm = g.getFontMetrics(); double adjust = minStem / minWidth; // double maxAcross = 0.0; for (int i = 0; i < tipCount; i++) { Stem nextTip = tips[i]; double span = adjust * nextTip.depth; if (genomeTips) { span += fm.stringWidth(nextTip.genome); } else { span += fm.stringWidth(nextTip.tip); } span += 0.5 * fm.getHeight(); maxDepth = Math.max(maxDepth, span); } int wideSet = (int) (maxDepth + minStem + 1.0); return (new Dimension(wideSet, fm.getHeight() * (tipCount + 2))); } private class Stem implements Comparable<Stem> { double width = 0.0; double depth = 0.0; // link to parent stem Stem above = null; // locus text for end of branch String tip = null; // genome text for end of branch String genome = null; // vertical position for top and bottom branches // for child branches double top = Double.MAX_VALUE; double base = 0.0; // ancesstory level // 0 corresponds to root // otherwise level = 1 + (above.level) int level = 0; Stem() { } Stem(int level, Stem above) { this.level = level; this.above = above; } Stem(int level, Stem above, int start, int end) { this.level = level; this.above = above; tip = nexus.substring(start, end); } void addWidth(int start, int end) { width = Double.parseDouble(nexus.substring(start, end)); if (above == null) { depth = width; } } void adjustBounds(double value) { if (value < top) { top = value; } if (base < value) { base = value; } } void buildDepth() { if (above != null) { this.depth = above.depth + this.width; } } void setDrop(double value) { top = value; base = value; if (above != null) { above.adjustBounds(value); } } void sendSpan() { if ((tip == null) && (above != null)) { above.adjustBounds(0.5 * (top + base)); } } int getTipRoom(FontMetrics fm) { int result = 0; if (tip != null) { if (genomeTips) { result = fm.stringWidth(genome); } else { result = fm.stringWidth(tip); } } return result; } void paint(double xExpand, double yExpand, int textDrop, int textGap, Graphics g) { int drop = (int) (0.5 + 0.5 * yExpand * (top + base)); if (flushTips) { int leftStem = (int) (0.5 + xExpand * level); if (0.0 < depth) { String length = "" + width; if (distChop < length.length()) { length = length.substring(0, distChop); } g.drawString(length, leftStem + textGap, drop); } if (tip == null) { int rightStem = (int) (0.5 + xExpand * (1 + level)); g.drawLine(leftStem, drop, rightStem, drop); int spanTop = (int) (0.5 + yExpand * top); int spanBase = (int) (0.5 + yExpand * base); g.drawLine(rightStem, spanTop, rightStem, spanBase); } else { int rightStem = (int) (0.5 + xExpand * maxLevel); g.drawLine(leftStem, drop, rightStem, drop); textDrop += drop; if (genomeTips) { g.drawString(genome, rightStem + textGap, textDrop); } else { g.drawString(tip, rightStem + textGap, textDrop); } } } else { double right = depth + minWidth; double left = depth; if (0.0 < depth) { left = right - width; } int leftStem = (int) (0.5 + xExpand * left); int rightStem = (int) (0.5 + xExpand * right); g.drawLine(leftStem, drop, rightStem, drop); if (tip == null) { int spanTop = (int) (0.5 + yExpand * top); int spanBase = (int) (0.5 + yExpand * base); g.drawLine(rightStem, spanTop, rightStem, spanBase); } else { textDrop += drop; if (genomeTips) { g.drawString(genome, rightStem + textGap, textDrop); } else { g.drawString(tip, rightStem + textGap, textDrop); } } } } public int compareTo(Stem arg0) { return (this.level - arg0.level); } } private class TipFinder implements Comparable<TipFinder> { String tipText; int tipAt; TipFinder(int index, String name) { tipAt = index; tipText = name; } public int compareTo(TipFinder arg0) { return ((this.tipText).compareTo(arg0.tipText)); } } }