/* * ScaleAxis.java * * Copyright (C) 2006-2014 Andrew Rambaut * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package figtree.treeviewer; import java.text.DecimalFormat; /** * * @author Andrew Rambaut * @version $Id$ * * $HeadURL$ * * $LastChangedBy$ * $LastChangedDate$ * $LastChangedRevision$ */ public class ScaleAxis { // The minimum and maximum values of the data // These constants are used for automatic scaling to select exactly // where the axis starts and stops. static public final int AT_MAJOR_TICK=0; static public final int AT_MAJOR_TICK_PLUS=1; static public final int AT_MINOR_TICK=2; static public final int AT_MINOR_TICK_PLUS=3; static public final int AT_DATA=4; static public final int AT_ZERO=5; static public final int AT_VALUE=6; protected double minData=Double.POSITIVE_INFINITY; protected double maxData=Double.NEGATIVE_INFINITY;// The number of major ticks and minor ticks within them protected int majorTickCount; protected int minorTickCount; // calculated automatically // The prefered minimum number of ticks protected int prefMajorTickCount = 5; protected int prefMinorTickCount = 2; // set manually // Flags using the above constants protected int minAxisFlag; protected int maxAxisFlag;// The distance between major ticks and minor Ticks protected double majorTick; protected double minorTick; // calculated automatically or set by user // The value of the first and last major tick protected double minTick; protected double maxTick; // calculated automatically or set by user // The value of the beginning and end of the axis protected double minAxis; protected double maxAxis;// User defined axis range protected double minValue; protected double maxValue;// Flags to give automatic scaling and integer division protected boolean isAutomatic = true; protected boolean isAutomaticTickSpacing = true; protected boolean isDiscrete=false;// Flags to specify that the first tick and last tick should have labels. // It is up to the AxisPanel to do something about this. protected boolean labelFirst=false; protected boolean labelLast=false; protected boolean isCalibrated = false; protected DecimalFormat formatter = new DecimalFormat("0.0#######");// Used internally private double epsilon; private int fraction; static private final int UNIT=0; static private final int HALFS=1; static private final int QUARTERS=2; static private final int FIFTHS=3; /** * Empty constructor */ public ScaleAxis() { } /** * Axis flag constructor */ public ScaleAxis(int minAxisFlag, int maxAxisFlag) { setAxisFlags(minAxisFlag, maxAxisFlag); } /** * Transform a value */ public double transform(double value) { return value; // a linear transform ! } /** * Untransform a value */ public double untransform(double value) { return value; // a linear transform ! } /** * Set axis flags */ public void setAxisFlags(int minAxisFlag, int maxAxisFlag) { this.minAxisFlag = minAxisFlag; this.maxAxisFlag = maxAxisFlag; isCalibrated = false; } /** * Set preferred number of ticks */ public void setPrefNumTicks(int prefMajorTickCount, int prefMinorTickCount) { this.prefMajorTickCount = prefMajorTickCount; this.prefMinorTickCount = prefMinorTickCount; isCalibrated = false; } /** * Set integer scale */ public void setIsDiscrete(boolean isDiscrete) { this.isDiscrete = isDiscrete; isCalibrated = false; } /** * Set show label for first tick flag */ public void setLabelFirst(boolean labelFirst) { this.labelFirst = labelFirst; } /** * Set show label for last tick flag */ public void setLabelLast(boolean labelLast) { this.labelLast = labelLast; } /** * Set the formatter for the tick labels */ public void setFormatter(DecimalFormat formatter) { this.formatter = formatter; } /** * return show label for first tick flag */ public boolean getLabelFirst() { if (getMinorTickCount(-1)==0) // The first tick is a label anyway return false; else return labelFirst; } /** * return show label for last tick flag */ public boolean getLabelLast() { if (getMinorTickCount(majorTickCount-1)==0) // The last tick is a label anyway return false; else return labelLast; } /** * Manually set the axis range. Axis flags must be set to AT_VALUE for this to take effect. */ public void setManualRange(double minValue, double maxValue) { this.minValue = minValue; this.maxValue = maxValue; isCalibrated = false; } /** * Manually set the axis ticks */ public void setManualAxis(double majorTick, double minorTick) { this.majorTick = majorTick; this.minorTick = minorTick; isAutomatic = true; isAutomaticTickSpacing = false; isCalibrated = false; } /** * Manually set the axis ticks */ public void setManualAxis(double minTick, double maxTick, double majorTick, double minorTick) { this.minTick = minTick; this.maxTick = maxTick; this.majorTick = majorTick; this.minorTick = minorTick; majorTickCount = (int)((maxTick-minTick)/majorTick)+1; // Add 1 to include the last tick minorTickCount = (int)(majorTick/minorTick)-1; // Sub 1 to exclude the major tick isAutomatic=false; isAutomaticTickSpacing = false; isCalibrated = false; } /** * Set the axis to automatic calibration */ public void setAutomatic() { setAutomatic(AT_MAJOR_TICK, AT_MAJOR_TICK); } /** * Set the axis to automatic calibration */ public void setAutomatic(int minAxisFlag, int maxAxisFlag) { setAxisFlags(minAxisFlag, maxAxisFlag); isAutomatic = true; isAutomaticTickSpacing = true; isCalibrated = false; } /** * Set the range of the data */ public void setRange(double minValue, double maxValue) { if (!Double.isNaN(minValue)) { this.minData = minValue; } if (!Double.isNaN(maxValue)) { this.maxData = maxValue; } isCalibrated = false; } /** * Adds the range to the existing range, widening if neccessary */ public void addRange(double minValue, double maxValue) { if (!Double.isNaN(maxValue) && maxValue > maxData) { maxData = maxValue; } if (!Double.isNaN(minValue) && minValue < minData) { minData = minValue; } //System.err.println("addRange("+minValue +", "+maxValue+")"); //System.err.println("maxValue = "+maxData); //System.err.println("maxData = "+maxData); isCalibrated = false; } /** * A static method that uses the natural log to obtain log to base10. * This is required for the linear autoCalibrate but will also be * used by a derived class giving a log transformed axis. */ static public double log10(double inValue) { return Math.log(inValue)/Math.log(10.0); } public void calibrate() { double minValue = minData; double maxValue = maxData; if (minAxisFlag== AT_ZERO) { minValue = 0; } else if (minAxisFlag == AT_VALUE) { minValue = this.minValue; } if (maxAxisFlag== AT_ZERO) { maxValue = 0; } else if (maxAxisFlag == AT_VALUE) { maxValue = this.maxValue; } double range = maxValue - minValue; if (range < 0.0) { range = 0.0; } epsilon = range * 1.0E-10; if (isAutomatic) { // We must find the optimum minMajorTick and maxMajorTick so // that they contain the data range (minData to maxData) and // are in the right order of magnitude if (range < 1.0E-30) { if (minData < 0.0) { if (isAutomaticTickSpacing) { majorTick = Math.pow(10.0, Math.floor(log10(Math.abs(minData)))); } minTick = Math.floor(minData / majorTick) * majorTick; maxTick = 0.0; } else if (minData > 0.0) { if (isAutomaticTickSpacing) { majorTick = Math.pow(10.0, Math.floor(log10(Math.abs(minData)))); } minTick = 0.0; maxTick = Math.ceil(maxData / majorTick) * majorTick; } else { if (isAutomaticTickSpacing) { majorTick = 1.0; } minTick = -1.0; maxTick = 1.0; } if (isAutomaticTickSpacing) { minorTick = majorTick; } majorTickCount = 1; minorTickCount = 0; } else { if (isAutomaticTickSpacing) { // First find order of magnitude below the data range... majorTick = Math.pow(10.0, Math.floor(log10(range))); } calcMinTick(); calcMaxTick(); majorTickCount=(int)(maxTick-minTick/majorTick); minorTickCount=(int)(majorTick/minorTick); if (isAutomaticTickSpacing) { calcMajorTick(); calcMinorTick(); } } } minAxis = minTick; maxAxis = maxTick; handleAxisFlags(); isCalibrated=true; } /** * Calculate the optimum minimum tick. Override to change default behaviour */ public void calcMinTick() { // Find the nearest multiple of majorTick below minData if (minData == 0.0) minTick = 0; else minTick = Math.floor(minData / majorTick) * majorTick; } /** * Calculate the optimum maximum tick. Override to change default behaviour */ public void calcMaxTick() { // Find the nearest multiple of majorTick above maxData if (maxData == 0) { maxTick = 0; } else if (maxData < 0.0) { // Added so that negative values are handled correctly -- AJD maxTick = -Math.floor(-maxData / majorTick) * majorTick; } else { maxTick = Math.ceil(maxData / majorTick) * majorTick; } } /** * Calculate the optimum major tick distance. Override to change default behaviour */ public void calcMajorTick() { fraction= UNIT; // make sure that there are at least prefNumMajorTicks major ticks // by dividing up into halves, quarters, fifths or tenths double u=majorTick; double r=maxTick-minTick; majorTickCount=(int)(r/u); while (majorTickCount < prefMajorTickCount) { u=majorTick/2; // Try using halves if (!isDiscrete || u==Math.floor(u)) { // u is an integer majorTickCount=(int)(r/u); fraction= HALFS; if (majorTickCount >= prefMajorTickCount) break; } u=majorTick/4; // Try using quarters if (!isDiscrete || u==Math.floor(u)) { // u is an integer majorTickCount=(int)(r/u); fraction= QUARTERS; if (majorTickCount >= prefMajorTickCount) break; } u=majorTick/5; // Try using fifths if (!isDiscrete || u==Math.floor(u)) { // u is an integer majorTickCount=(int)(r/u); fraction= FIFTHS; if (majorTickCount >= prefMajorTickCount) break; } if (isDiscrete && (majorTick/10)!=Math.floor(majorTick/10)) { // majorTick/10 is not an integer so no point in further subdivision u=majorTick; majorTickCount=(int)(r/u); break; } majorTick/=10; // finally just divide by ten u=majorTick; // and go back to whole units majorTickCount=(int)(r/u); fraction= UNIT; } majorTick=u; if (isDiscrete && majorTick<1.0) { majorTick=1.0; majorTickCount=(int)(r/majorTick); fraction= UNIT; } majorTickCount++; // Add 1 to give the final tick // Trim down any excess major ticks either side of the data range // Epsilon allows for any inprecision in the calculation while ((minTick + majorTick - epsilon)<minData) { minTick+=majorTick; majorTickCount--; } while ((maxTick - majorTick + epsilon)>maxData) { maxTick-=majorTick; majorTickCount--; } } /** * Calculate the optimum minor tick distance. Override to change default behaviour */ public void calcMinorTick() { minorTick=majorTick; // start with minorTick the same as majorTick double u=minorTick; double r=majorTick; minorTickCount=(int)(r/u); while (minorTickCount < prefMinorTickCount) { // if the majorTick was divided as quarters, then we can't // divide the minor ticks into halves or quarters. if (fraction!= QUARTERS) { u=minorTick/2; // Try using halves if (!isDiscrete || u==Math.floor(u)) { // u is an integer minorTickCount=(int)(r/u); if (minorTickCount>=prefMinorTickCount) break; } u=minorTick/4; // Try using quarters if (!isDiscrete || u==Math.floor(u)) { // u is an integer minorTickCount=(int)(r/u); if (minorTickCount>=prefMinorTickCount) break; } } u=minorTick/5; // Try using fifths if (!isDiscrete || u==Math.floor(u)) { // u is an integer minorTickCount=(int)(r/u); if (minorTickCount>=prefMinorTickCount) break; } if (isDiscrete && (minorTick/10)!=Math.floor(minorTick/10)) { // minorTick/10 is not an integer so no point in further subdivision u=minorTick; minorTickCount=(int)(r/u); break; } minorTick/=10; // finally just divide by ten u=minorTick; // and go back to whole units minorTickCount=(int)(r/u); } minorTick=u; minorTickCount--; } /** * Handles axis flags. Override to change default behaviour */ public void handleAxisFlags() { // Now we must honor the min/maxAxisFlag settings if (minAxisFlag== AT_MAJOR_TICK_PLUS || minAxisFlag== AT_MINOR_TICK_PLUS) { if (minAxis==minData) { majorTickCount++; minTick-=majorTick; minAxis=minTick; } } if (minAxisFlag== AT_MINOR_TICK_PLUS) { if ((minAxis+minorTick)<minData) { majorTickCount--; minTick+=majorTick; while ((minAxis+minorTick)<minData) { minAxis+=minorTick; } } } else if (minAxisFlag== AT_MINOR_TICK) { if ((minAxis+minorTick)<=minData) { majorTickCount--; minTick+=majorTick; while ((minAxis+minorTick)<=minData) { minAxis+=minorTick; } } } else if (minAxisFlag== AT_DATA) { if (minTick<minData) { // in case minTick==minData majorTickCount--; minTick+=majorTick; } minAxis=minData; } else if (minAxisFlag== AT_VALUE) { if (minTick<minValue) { // in case minTick==minValue majorTickCount--; minTick+=majorTick; } minAxis=minValue; } else if (minAxisFlag== AT_ZERO) { majorTickCount+=(int)(minTick/majorTick); minTick=0; minAxis=0; } if (maxAxisFlag== AT_MAJOR_TICK_PLUS || maxAxisFlag== AT_MINOR_TICK_PLUS) { if (maxAxis==maxData) { majorTickCount++; maxTick+=majorTick; maxAxis=maxTick; } } if (maxAxisFlag== AT_MINOR_TICK_PLUS) { if ((maxAxis-minorTick)>maxData) { majorTickCount--; maxTick-=majorTick; while ((maxAxis-minorTick)>maxData) { maxAxis-=minorTick; } } } else if (maxAxisFlag== AT_MINOR_TICK) { if ((maxAxis-minorTick)>=maxData) { majorTickCount--; maxTick-=majorTick; while ((maxAxis-minorTick)>=maxData) { maxAxis-=minorTick; } } } else if (maxAxisFlag== AT_DATA) { if (maxTick>maxData) { // in case maxTick==maxData majorTickCount--; maxTick-=majorTick; } maxAxis=maxData; } else if (maxAxisFlag== AT_VALUE) { if (maxTick>maxValue) { // in case maxTick==maxValue majorTickCount--; maxTick-=majorTick; } maxAxis=maxValue; } else if (maxAxisFlag== AT_ZERO) { majorTickCount+=(int)(-maxTick/majorTick); maxTick=0; maxTick=0; } } /** * Scale a value to between 0 and 1. */ public double scaleValue(double value) { if (!isCalibrated) calibrate(); double f=(transform(value)-transform(minAxis))/(transform(maxAxis)-transform(minAxis)); return f; } /** * @return a DecimalFormat for formating the axis labels */ public DecimalFormat getFormatter() { return formatter; } /** * @return minimum range of the axis */ public double getMinAxis() { if (!isCalibrated) calibrate(); return minAxis; } /** * @return maximum range of the axis */ public double getMaxAxis() { if (!isCalibrated) calibrate(); return maxAxis; } /** * @return minimum range of the data */ public double getMinData() { return minData; } /** * @return maximum range of the data */ public double getMaxData() { return maxData; } /** * Returns the number of major tick marks along the axis */ public int getMajorTickCount() { if (!isCalibrated) calibrate(); return majorTickCount; } /** * Returns the number of minor tick marks within each major one * By default all major ticks have the same number of minor ticks * except the last which has none. */ public int getMinorTickCount(int majorTickIndex) { if (!isCalibrated) calibrate(); if (minorTick <= 0) { return 0; } if (majorTickIndex == majorTickCount-1) return (int)((maxAxis-maxTick)/minorTick); else if (majorTickIndex==-1) return (int)((minTick-minAxis)/minorTick); else return minorTickCount; } /** getMajorTick * Returns the value of the majorTickIndex'th major tick */ public double getMajorTickValue(int majorTickIndex) { if (!isCalibrated) calibrate(); return (majorTickIndex*majorTick)+minTick; } /** getMinorTick * Returns the value of the minorTickIndex'th minor tick */ public double getMinorTickValue(int minorTickIndex, int majorTickIndex) { if (!isCalibrated) calibrate(); // get minorTickIndex+1 to skip the major tick if (majorTickIndex==-1) return minTick-((minorTickIndex+1)*minorTick); else return ((minorTickIndex+1)*minorTick)+getMajorTickValue(majorTickIndex); } /** * @return the spacing between major ticks */ public double getMajorTickSpacing() { if (!isCalibrated) calibrate(); return majorTick; } /** * @return the spacing between minor ticks */ public double getMinorTickSpacing() { if (!isCalibrated) calibrate(); return minorTick; } }