package jas.plot; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; /** * An axis type for representing times. * <p>Note that this is not the same as dates. A date represents a particular * point in time (e.g., when an event took place). This axis represents * time values, such as the time between two events. Therefore, while a date * value may be represented as "Jan 31, 1994", a time value might be represented as "3 weeks". * @see DateAxis * @author Jonas Gifford */ final public class TimeAxis extends AxisType implements TimeCoordinateTransformation { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin code for handling unit size */ /** The integer code for millisecond units. */ public static final int MILLISECONDS = 0; /** The integer code for second units. */ public static final int SECONDS = 1; /** The integer code for minute units. */ public static final int MINUTES = 2; /** The integer code for hour units. */ public static final int HOURS = 3; /** The integer code for day units. */ public static final int DAYS = 4; /** The integer code for week units. */ public static final int WEEKS = 5; /** The integer code for month units. */ public static final int MONTHS = 6; /** The integer code for year units. */ public static final int YEARS = 7; /** * Set <code>OMIT</code> as the length of a time unit to have that unit not * considered as a candidate for axis units. */ public static final long OMIT = 0L; private long[] unitLengths = { 1L, // milliseconds 1000L, // seconds 1000L * 60L, // minutes 1000L * 60L * 60L, // hours 1000L * 60L * 60L * 24L, // days 1000L * 60L * 60L * 24L * 7L, // weeks OMIT, // months 1000L * 60L * 60L * 24L * 365L // years }; /** * Allows units to be viewed as valued different from the default. For example, * a year by default is <code>1000L * 60L * 60L * 24L * 365L</code> milliseconds, * but a call such as this may be desirable:<br> * <code>setUnitLength(TimeAxis.YEARS, (long) (1000 * 60 * 60 * 24 * 365.24));</code> */ public void setUnitLength(final int unit, final long length) { if (length < 0L || length != OMIT && (unit != MILLISECONDS && length <= unitLengths[unit - 1] || unit != YEARS && length >= unitLengths[unit + 1])) { throw new IllegalArgumentException(); } if (length == OMIT && unitIndex == unit) labelsValid = false; // these labels are no good, so on the next validation we'll get a new set unitLengths[unit] = length; } /** * Returns the number of milliseconds for this unit, or <code>OMIT</code>. * @see #OMIT */ public long getUnitLength(int unit) { return unitLengths[unit]; } /* * end code for handling unit size * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin interface to unit description */ private final String[] unitNames = { "milliseconds", "seconds", "minutes", "hours", "days", "weeks", "months", "years" }; /** Returns a string representation of the units showing on the axis. */ public String getUnits() { return unitNames[unitIndex]; } /* * end interface to unit description * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** * Sets whether this object should round the minimum down and the maximum * up to make labels land exactly on the min and max of the axis range. */ public void setUseSuggestedRange(final boolean useSuggestedRange) { if (this.useSuggestedRange != useSuggestedRange) labelsValid = false; // these labels are no good, so on the next validation we'll get a new set this.useSuggestedRange = useSuggestedRange; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin interface to range */ /** Sets the minimum value for the axis data. */ public void setMin(final long min) { if (dataMin != min) labelsValid = false; // these labels are no good, so on the next validation we'll get a new set dataMin = min; } /** Sets the maximum value for the axis data. */ public void setMax(final long max) { if (dataMax != max) labelsValid = false; // these labels are no good, so on the next validation we'll get a new set dataMax = max; } /** * Returns the minimum value on the axis range. This value may be * smaller than the data minimum if the axis has been told to use the * suggested range. * @see #setUseSuggestedRange(boolean) * @see #setMin(long) * @see #getDataMin() */ public long getAxisMin() { return axisMin; } /** * Returns the maximum value on the axis range. This value may be * larger than the data maximum if the axis has been told to use the * suggested range. * @see #setUseSuggestedRange(boolean) * @see #setMax(long) * @see #getDataMax() */ public long getAxisMax() { return axisMax; } /** * Returns the minimum value on the data range, as set by the method * <code>setMin(long)</code>. This value may be * larger than the axis minimum if the axis has been told to use the * suggested range. * @see #setMin(long) * @see #setUseSuggestedRange(boolean) * @see #getAxisMin() */ public long getDataMin() { return dataMin; } /** * Returns the maximum value on the axis range, as set by the method * <code>setMax(long)</code>. This value may be * smaller than the axis maximum if the axis has been told to use the * suggested range. * @see #setMax(long) * @see #setUseSuggestedRange(boolean) * @see #getAxisMax() */ public long getDataMax() { return dataMax; } /* * end interface to range * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin AxisType methods */ /** * Returns an instance of TimeCoordinateTransformation. * @see TimeCoordinateTransformation */ CoordinateTransformation getCoordinateTransformation() { return this; } void paintAxis(final PlotGraphics g, final double originX, final double originY, final double axisLength, final Color textColor, final Color majorTickColor, final Color minorTickColor) { final FontMetrics fm = g.getFontMetrics(); if (axis.getAxisOrientation() == Axis.HORIZONTAL) { final double y = originY + fm.getMaxAscent() + Axis.padFromAxis; for (int i = 0; i < labels.length; i++) { final String text = labels[i].text; final double x = originX + labels[i].position * axisLength; g.setColor(textColor); g.drawString(text, x - fm.stringWidth(text) / 2, y); g.setColor(majorTickColor); g.drawLine(x, originY + majorTickLength, x, originY - majorTickLength); } } else { final double x = axis.onLeftSide ? originX - Axis.padFromAxis : originX + Axis.padFromAxis; final double height = fm.getAscent() / 2; for (int i = 0; i < labels.length; i++) { final String text = labels[i].text; final double y = originY - labels[i].position * axisLength; g.setColor(textColor); g.drawString(text, axis.onLeftSide ? x - fm.stringWidth(text) : x, y + height); g.setColor(majorTickColor); g.drawLine(originX - majorTickLength, y, originX + majorTickLength, y); } } } // The method below uses several calculations with the same idea: // Math.max(<some distance>, 0); // The integer <some distance> represents the distance that the // label goes past the end of the axis. If the label // doesn't go past the end of the axis, then <some distance> // would be negative, in which case the flow past the end is 0. void assumeAxisLength(final int axisLength) { Font font = axis.getFont(); final FontMetrics fm = axis.getToolkit().getFontMetrics(font); final int maxNumberOfDivisions = getMaxNumberOfDivisions(fm, axisLength); if (!labelsValid || labels == null || labels.length > maxNumberOfDivisions || labels.length < maxNumberOfDivisions / 2) createNewLabels(maxNumberOfDivisions); if (axis.getAxisOrientation() == Axis.VERTICAL) { spaceRequirements.width = longestStringLength(fm,labels) + Axis.padFromAxis; spaceRequirements.height = Math.max(fm.getAscent() / 2 - (int) (labels[0].position * axisLength), 0); // numbers only, so no descent spaceRequirements.flowPastEnd = Math.max(fm.getMaxAscent() - fm.getAscent() / 2 - (int) ((1.0 - labels[labels.length - 1].position) * axisLength), 0); } else { spaceRequirements.width = Math.max(fm.stringWidth(labels[0].text) / 2 - (int) (labels[0].position * axisLength), 0); spaceRequirements.height = fm.getMaxAscent() + Axis.padFromAxis; // numbers only, so no descent spaceRequirements.flowPastEnd = Math.max(fm.stringWidth(labels[labels.length - 1].text) / 2 - (int) ((1.0 - labels[labels.length - 1].position) * axisLength), 0); } } int getMajorTickMarkLength() { return majorTickLength; } /* * end AxisType methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin private methods */ private void createNewLabels(final int maxNDivisions) { labelsValid = true; unitIndex = getUnitIndex((dataMax - dataMin) / (long) maxNDivisions); final long clumpLength = getClumpSize((dataMax - dataMin) / unitLengths[unitIndex] / (long) maxNDivisions); if (useSuggestedRange) // make axisMin the next smallest multiple of clumpLength (it may stay the same if it is already a multiple) axisMin = dataMin - dataMin % clumpLength; else // axisMin is the same as dataMin because we don't adjust the range axisMin = dataMin; if (useSuggestedRange && dataMax % clumpLength != 0L) // make axisMax the next largest multiple of clumpLength axisMax = dataMax + clumpLength - dataMax % clumpLength; else // axisMax is the same as dataMax either because we don't adjust the range or // because it is already a multiple of clumpLength axisMax = dataMax; int nLabels = (int) ((axisMax - axisMin) / clumpLength); if (useSuggestedRange || axisMin % clumpLength == 0 || axisMin % clumpLength > axisMax % clumpLength) nLabels++; labels = new AxisLabel[nLabels]; long labelValue = useSuggestedRange || axisMin % clumpLength == 0 ? axisMin : axisMin + clumpLength - axisMin % clumpLength; for (int i = 0; i < nLabels; i++) { labels[i] = new AxisLabel(); labels[i].text = String.valueOf(labelValue / unitLengths[unitIndex]); labels[i].position = (double) (labelValue - axisMin) / (double) (axisMax - axisMin); labelValue += clumpLength; } System.out.println("Labels are using units: ".concat(getUnits())); } private int getMaxNumberOfDivisions(final FontMetrics fm, final int axisLength) { int result; if (axis.getAxisOrientation() == Axis.HORIZONTAL) result = axisLength / (fm.charWidth('5') * maxCharsPerLabel + minSpaceBetweenLabels); // we assume '5' has typical width else result = axisLength / (fm.getHeight() + minSpaceBetweenLabels); // we have at least two divisions return Math.max(2, result); } // @param minDivisionSpan the fewest number of milliseconds in a division // @return the index corresponding to the units we want to use private int getUnitIndex(long minDivisionSpan) { // We double minDivisionSpan because the cutoff we want to use is actually half of the unit length. Suppose // for example our minDivisionSpan is 0.8 minutes, or some value close to a minute. If we didn't double // minDivisionSpan, our units would be seconds, but that would be silly because we could very easily // just have one-minute intervals between labels. Therefore, we want the cutoff for minutes to be half // of a minute instead of a full minute so that values for minDivisionSpan as small as 30 seconds will // result in a minute units. Doubling minDivision span has the same effect on the control statements // below as halving all of the unit lengths, but is more efficient because it involves only one calculation // instead of one per iteration. minDivisionSpan *= 2L; // i is the index of a trial unit int i = 0; // j is the index of the largest acceptable unit int j = 0; while (i < unitLengths.length) { if (unitLengths[i] == OMIT) // we can't inclulde this unit, so... { // we skip to the next one i++; continue; } if (unitLengths[i] < minDivisionSpan) // this one is acceptable, so we { // set j to the acceptable value of i (the value before incrementing), and go to the next iteration j = i++; continue; } // if we get to this point, there are no more acceptable indexes, so we will break and return j break; } // return the highest acceptable value return j; } // @param naturalNumberInClump the number of units (not necessarily milliseconds) in a clump // (we will find the next largest value and return it in milliseconds) private long getClumpSize(long naturalNumberInClump) // a clump is the number of units per tick mark { // these are the clump sizes we will try final long[] typicalClumps = {1L, 2L, 5L, 10L, 20L, 25L, 50L, 100L, 200L}; // mult is a scale factor we use to handle clumps independently from unit size and order of magnitude long mult = unitLengths[unitIndex]; // if the natural number in the clump is greater than 100 we want to divide it // up such that we have a number within the inclusive range 0:100 while (naturalNumberInClump > 100L) { naturalNumberInClump /= 100L; // we increase mult to keep track of how we have scaled mult *= 100L; } int clumpIndex = 0; while (naturalNumberInClump >= typicalClumps[clumpIndex]) clumpIndex++; // we multiply by mult, se we get the number of milliseconds in a clump return typicalClumps[clumpIndex] * mult; } /* * end private methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin coordinate transformation methods */ public double convert(long time) { final int minL = axis.getMinLocation(); final int maxL = axis.getMaxLocation(); final float f = (float) (time - axisMin) / (float) (axisMax - axisMin); return minL + f * (maxL - minL); } /* * end coordinate transformation methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin externalization methods */ public void writeExternal(final ObjectOutput out) throws IOException { out.writeBoolean(useSuggestedRange); out.writeObject(unitLengths); } public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { useSuggestedRange = in.readBoolean(); unitLengths = (long[]) in.readObject(); } /* * end externalization methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin members */ /* variables */ private boolean useSuggestedRange = false; private int unitIndex; private long dataMin, dataMax; private long axisMin, axisMax; private AxisLabel[] labels; /* constants */ private final int majorTickLength = 5; private final int minSpaceBetweenLabels = 4; private final int maxCharsPerLabel = 6; /* * end members * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ }