/******************************************************************************* * Mission Control Technologies, Copyright (c) 2009-2012, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * * The MCT platform is 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. * * MCT includes source code licensed under additional open source licenses. See * the MCT Open Source Licenses file included with this distribution or the About * MCT Licenses dialog available at runtime from the MCT Help menu for additional * information. *******************************************************************************/ package plotter.xy; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.text.NumberFormat; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.Set; import plotter.AxisLabel; import plotter.DateNumberFormat; import plotter.LinearTickMarkCalculator; import plotter.MultiLineLabelUI; import plotter.Rotation; import plotter.TickMarkCalculator; /** * A linear XY axis is the normal case where points on the screen are linearly related to their corresponding data values. * @author Adam Crume */ public class LinearXYAxis extends XYAxis { private static final long serialVersionUID = 1L; /** Start value of the axis at the time the labels were cached. */ private double labelCacheStart = Double.NaN; /** End value of the axis at the time the labels were cached. */ private double labelCacheEnd = Double.NaN; /** Values of the major tick marks. */ private double[] majorVals; /** Values of the minor tick marks. */ private double[] minorVals; /** Calculates tick marks and labels for the axis. */ private TickMarkCalculator tickMarkCalculator = new LinearTickMarkCalculator(); /** Format used to display values in labels. */ private NumberFormat format = NumberFormat.getInstance(); /** Time system axis label name. */ private String timeSystemAxisLabelName; /** * Creates an axis. * @param d dimension the axis represents */ public LinearXYAxis(XYDimension d) { super(d); } @Override public void doLayout() { super.doLayout(); double start = getStart(); double end = getEnd(); double diff = end - start; XYDimension plotDimension = getPlotDimension(); int startMargin = getStartMargin(); int width = getWidth(); int height = getHeight(); int size = (plotDimension == XYDimension.X ? width : height) - startMargin - getEndMargin(); // Labels only need to be recreated if the min or max changes. // Otherwise, they simply need to be repositioned. if(start != labelCacheStart || end != labelCacheEnd) { double[][] ticks = tickMarkCalculator.calculateTickMarks(this); majorVals = ticks[0]; minorVals = ticks[1]; labelCacheStart = start; labelCacheEnd = end; } // Calculate the physical locations of the tick marks from the logical locations. int[] major = new int[majorVals.length]; int[] minor = new int[minorVals.length]; for(int i = 0; i < major.length; i++) { major[i] = (int) ((majorVals[i] - start) / diff * size + .5) - 1; } for(int i = 0; i < minor.length; i++) { minor[i] = (int) ((minorVals[i] - start) / diff * size + .5) - 1; } setMajorTicks(major); setMinorTicks(minor); if(isShowLabels()) { updateLabels(); } else { removeAll(); } } /** * Updates labels to match the tick marks. * Labels may be added, removed, or repositioned. */ private void updateLabels() { int[] major = getMajorTicks(); int width = getWidth(); int height = getHeight(); int startMargin = getStartMargin(); XYDimension plotDimension = getPlotDimension(); double diff = getEnd() - getStart(); Component[] components = getComponents(); // oldLabels contains the original labels that have not been reused. Set<AxisLabel> oldLabels = new HashSet<AxisLabel>(components.length); for(Component c : components) { oldLabels.add((AxisLabel) c); } // maxLabelError defines the largest absolute difference between a label's value and the needed value. // If the error is greater than this, the label is not reused. double maxLabelError = .01 * Math.abs(diff); AxisLabel[] labels = new AxisLabel[majorVals.length]; for(int i = 0; i < labels.length; i++) { labels[i] = createLabel(majorVals[i], oldLabels, maxLabelError); } // Remove labels that have not been reused. for(AxisLabel oldLabel : oldLabels) { remove(oldLabel); } // Position the labels to line up with the corresponding tick marks. int textMargin = getTextMargin(); boolean inverted = getEnd() < getStart(); if(plotDimension == XYDimension.X) { for(int i = 0; i < major.length; i++) { Component label = labels[i]; double preferredSize = label.getPreferredSize().getWidth(); double end = preferredSize / 2; double start = -end; if(inverted) { if(i < major.length - 1) { start = Math.max(start, (major[i + 1] - major[i]) / 2); } if(i > 0) { end = Math.min(end, (major[i - 1] - major[i]) / 2); } } else { if(i > 0) { start = Math.max(start, (major[i - 1] - major[i]) / 2); } if(i < major.length - 1) { end = Math.min(end, (major[i + 1] - major[i]) / 2); } } double labelWidth = end - start; label.setSize((int) labelWidth, label.getHeight()); label.setLocation((int) (start + startMargin + major[i]), textMargin); } } else { int height2 = height - startMargin; int width2 = width - textMargin; for(int i = 0; i < major.length; i++) { Component label = labels[i]; double preferredSize = label.getPreferredSize().getHeight(); double end = preferredSize / 2; double start = -end; if(inverted) { if(i < major.length - 1) { start = Math.max(start, (major[i + 1] - major[i]) / 2); } if(i > 0) { end = Math.min(end, (major[i - 1] - major[i]) / 2); } } else { if(i > 0) { start = Math.max(start, (major[i - 1] - major[i]) / 2); } if(i < major.length - 1) { end = Math.min(end, (major[i + 1] - major[i]) / 2); } } double labelHeight = end - start; int labelWidth = label.getWidth(); label.setSize(labelWidth, (int) labelHeight); label.setLocation(width2 - labelWidth, (int) (height2 - major[i] - end)); } } } /** * Creates a label for the given value. * A label may instead be reused (and removed) from the <code>oldLabels</code> set. * @param value value the label displays * @param oldLabels labels that may be reused * @param maxLabelError largest absolute difference between a label's value and <code>value</code> that allows a label to be reused * @return the label, which may be new or reused */ private AxisLabel createLabel(double value, Set<AxisLabel> oldLabels, double maxLabelError) { for(AxisLabel oldLabel : oldLabels) { // Value is the same, reuse the label. // It is currently assumed that if the value is close enough, the text doesn't change. // For well-behaved tick mark calculators and label formats, this should be true. (Use rounding!) // The new text could be calculated, but that is expensive. if(Math.abs(value - oldLabel.getValue()) < maxLabelError) { oldLabels.remove(oldLabel); return oldLabel; } } AxisLabel label = new AxisLabel(value, format.format(value)); if (format.getClass().equals(DateNumberFormat.class)) { GregorianCalendar gc = new GregorianCalendar(); gc.setTimeInMillis((long) value); label.setToolTipText(format.format(value) +" " + gc.get(Calendar.YEAR) ); } Rotation labelRotation = getLabelRotation(); if(labelRotation != null) { label.putClientProperty(Rotation.class.getName(), labelRotation); } label.setUI(MultiLineLabelUI.labelUI); label.setForeground(getForeground()); label.setFont(getFont()); add(label); label.setSize(label.getPreferredSize()); return label; } @Override public int toPhysical(double d) { double min = getStart(); double max = getEnd(); int endMargin = getEndMargin(); if(getPlotDimension() == XYDimension.X) { int width = getWidth(); int startMargin = getStartMargin(); return (int) ((d - min) / (max - min) * (width - startMargin - endMargin) + .5) + getStartMargin() - 1; } else { int height = getHeight() - getStartMargin(); return height - (int) ((d - min) / (max - min) * (height - endMargin) + .5); } } @Override public double toLogical(int n) { double min = getStart(); double max = getEnd(); int endMargin = getEndMargin(); if(getPlotDimension() == XYDimension.X) { int width = getWidth(); int startMargin = getStartMargin(); return (n - startMargin + 1) * 1.0 / (width - startMargin - endMargin) * (max - min) + min; } else { int height = getHeight() - getStartMargin() - endMargin; return (height - n + endMargin) * 1.0 / height * (max - min) + min; } } /** * Sets the foreground color. * Overridden to update the color of the labels. * @param color the foreground color */ @Override public void setForeground(Color color) { super.setForeground(color); for(Component c : getComponents()) { c.setForeground(color); } } /** * Sets the font. * Overridden to update the font of the labels. * @param font the font */ @Override public void setFont(Font font) { super.setFont(font); for(Component c : getComponents()) { c.setFont(font); } } /** * Returns the tick mark calculator. * @return the tick mark calculator */ public TickMarkCalculator getTickMarkCalculator() { return tickMarkCalculator; } /** * Sets the tick mark calculator. * @param tickMarkCalculator the tick mark calculator */ public void setTickMarkCalculator(TickMarkCalculator tickMarkCalculator) { this.tickMarkCalculator = tickMarkCalculator; } /** * Returns the format for the labels. * @return the format for the labels */ public NumberFormat getFormat() { return format; } /** * Sets the format for the labels. * @param format the format for the labels */ public void setFormat(NumberFormat format) { this.format = format; } @Override public String getTimeSystemAxisLabelName() { return timeSystemAxisLabelName; } @Override public void setTimeSystemAxisLabelName(String labelName) { timeSystemAxisLabelName = labelName; } }