/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* 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 com.andork.plot;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JComponent;
import org.andork.event.BasicPropertyChangeSupport.External;
import org.andork.event.HierarchicalBasicPropertyChangeSupport;
import org.andork.model.Model;
@SuppressWarnings("serial")
public class PlotAxis extends JComponent implements Model {
public static enum LabelPosition {
TOP, BOTTOM, LEFT, RIGHT;
}
public static enum Orientation {
HORIZONTAL, VERTICAL;
}
public static enum Property {
AXIS_CONVERSION;
}
/**
*
*/
private static final long serialVersionUID = 2336004416638839578L;
private static final HierarchicalBasicPropertyChangeSupport changeSupport = new HierarchicalBasicPropertyChangeSupport();
public static void equalizeScale(PlotAxis... axes) {
double scale = Double.MAX_VALUE;
for (PlotAxis axis : axes) {
scale = Math.min(scale, Math.abs(axis.getAxisConversion().getScale()));
}
for (PlotAxis axis : axes) {
LinearAxisConversion conv = axis.getAxisConversion();
if (axis.getViewSpan() == 0) {
conv.setScale(scale * Math.signum(conv.getScale()));
} else {
double start = conv.invert(0);
double end = conv.invert(axis.getViewSpan());
double mid = (start + end) * 0.5;
double newSpan = axis.getViewSpan() / scale;
if (start < end) {
conv.set(mid - newSpan / 2, 0, mid + newSpan / 2, axis.getViewSpan());
} else {
conv.set(mid + newSpan / 2, 0, mid - newSpan / 2, axis.getViewSpan());
}
}
}
for (PlotAxis axis : axes) {
axis.repaint();
for (Component plot : axis.plots) {
plot.repaint();
}
}
}
private LinearAxisConversion axisConversion = new LinearAxisConversion();
private final Set<Component> plots = new HashSet<Component>();
private final Orientation orientation;
private LabelPosition labelPosition;
private int majorTickSize = 10;
private int minorTickSize = 5;
private Color majorTickColor = Color.GRAY;
private Color minorTickColor = Color.LIGHT_GRAY;
private int minMinorGridLineSpacing = 30;
private int labelPadding = 3;
private Double minValueForCalcSizes;
private Double maxValueForCalcSizes;
private Dimension calcMinSize;
private Dimension calcPrefSize;
private NumberFormat format;
public PlotAxis(Orientation orientation, LabelPosition labelPosition) {
this.orientation = orientation;
setLabelPosition(labelPosition);
format = NumberFormat.getInstance();
format.setGroupingUsed(false);
}
public void addPlot(Plot plot) {
plots.add(plot);
}
@Override
public External changeSupport() {
return changeSupport.external();
}
@Override
public Object get(Object key) {
if (key == Property.AXIS_CONVERSION) {
return getAxisConversion();
}
throw new IllegalArgumentException("Invalid key: " + key);
}
public LinearAxisConversion getAxisConversion() {
return axisConversion;
}
public LabelPosition getLabelPosition() {
return labelPosition;
}
public Color getMajorTickColor() {
return majorTickColor;
}
public int getMajorTickSize() {
return majorTickSize;
}
@Override
public Dimension getMinimumSize() {
if (calcMinSize != null && !isMinimumSizeSet()) {
return new Dimension(calcMinSize);
}
return super.getMinimumSize();
}
public int getMinMinorGridLineSpacing() {
return minMinorGridLineSpacing;
}
public Color getMinorTickColor() {
return minorTickColor;
}
public int getMinorTickSize() {
return minorTickSize;
}
public Orientation getOrientation() {
return orientation;
}
public Set<Component> getPlots() {
return Collections.unmodifiableSet(plots);
}
@Override
public Dimension getPreferredSize() {
if (calcPrefSize != null && !isPreferredSizeSet()) {
return new Dimension(calcPrefSize);
}
return super.getPreferredSize();
}
public int getTextPadding() {
return labelPadding;
}
public int getViewSpan() {
int result = orientation == Orientation.HORIZONTAL ? getWidth() : getHeight();
if (result != 0) {
return result;
}
return orientation == Orientation.HORIZONTAL ? getPreferredSize().width : getPreferredSize().height;
}
@Override
protected void paintComponent(Graphics g) {
if (updateSizes((Graphics2D) g)) {
revalidate();
}
super.paintComponent(g);
Rectangle bounds = getBounds();
bounds.x = 0;
bounds.y = 0;
Insets insets = getInsets();
Graphics2D g2 = (Graphics2D) g;
Object origAntialiasing = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
Paint origPaint = g2.getPaint();
Stroke origStroke = g2.getStroke();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double minorSpacing = GridMath
.niceCeiling(Math.abs(axisConversion.invert(minMinorGridLineSpacing) - axisConversion.invert(0)));
double majorSpacing = minorSpacing * 2;
if (orientation == Orientation.VERTICAL) {
double topValue = axisConversion.invert(0);
double bottomValue = axisConversion.invert(getHeight());
int fractionDigits = GridMath.niceCeilingFractionDigits(majorSpacing);
format.setMinimumFractionDigits(fractionDigits);
format.setMaximumFractionDigits(fractionDigits);
Rectangle minorBounds = new Rectangle(bounds);
minorBounds.width = minorTickSize;
Rectangle majorBounds = new Rectangle(bounds);
majorBounds.width = majorTickSize;
Rectangle textBounds = new Rectangle(bounds);
textBounds.width = (int) Math
.ceil(PlotUtils.calcHorizontalGridLineLabelsWidth(g2, topValue, bottomValue, majorSpacing, format));
int alignment;
if (labelPosition == LabelPosition.LEFT) {
minorBounds.x = getWidth() - insets.right - minorBounds.width;
majorBounds.x = getWidth() - insets.right - majorBounds.width;
textBounds.x = majorBounds.x - textBounds.width - labelPadding;
alignment = PlotUtils.RIGHT;
} else {
minorBounds.x = insets.left;
majorBounds.x = insets.left;
textBounds.x = majorBounds.x + majorBounds.width + labelPadding;
alignment = PlotUtils.LEFT;
}
g2.setColor(minorTickColor);
PlotUtils.drawHorizontalGridLines(g2, minorBounds, topValue, bottomValue, minorSpacing);
g2.setColor(majorTickColor);
PlotUtils.drawHorizontalGridLines(g2, majorBounds, topValue, bottomValue, majorSpacing);
g2.setColor(getForeground());
PlotUtils.drawHorizontalGridLineLabels(g2, textBounds, alignment, topValue, bottomValue, majorSpacing,
format);
} else {
double leftDomain = axisConversion.invert(0);
double rightDomain = axisConversion.invert(getWidth());
Rectangle minorBounds = new Rectangle(bounds);
minorBounds.height = minorTickSize;
Rectangle majorBounds = new Rectangle(bounds);
majorBounds.height = majorTickSize;
Rectangle textBounds = new Rectangle(bounds);
textBounds.height = g2.getFontMetrics().getAscent();
if (labelPosition == LabelPosition.TOP) {
minorBounds.y = getHeight() - insets.bottom - minorBounds.height;
majorBounds.y = getHeight() - insets.bottom - majorBounds.height;
textBounds.y = majorBounds.y - textBounds.height - labelPadding;
} else {
minorBounds.y = insets.top;
majorBounds.y = insets.top;
textBounds.y = majorBounds.y + majorBounds.height + labelPadding;
}
g2.setColor(minorTickColor);
PlotUtils.drawVerticalGridLines(g2, minorBounds, leftDomain, rightDomain, minorSpacing);
g2.setColor(majorTickColor);
PlotUtils.drawVerticalGridLines(g2, majorBounds, leftDomain, rightDomain, majorSpacing);
NumberFormat format = NumberFormat.getInstance();
int fractionDigits = GridMath.niceCeilingFractionDigits(majorSpacing);
format.setMinimumFractionDigits(fractionDigits);
format.setMaximumFractionDigits(fractionDigits);
format.setGroupingUsed(false);
g2.setColor(getForeground());
PlotUtils.drawVerticalGridLineLabels(g2, textBounds, leftDomain, rightDomain, majorSpacing, format);
}
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, origAntialiasing);
g2.setPaint(origPaint);
g2.setStroke(origStroke);
}
public void removePlot(Plot plot) {
plots.remove(plot);
}
@Override
public void set(Object key, Object newValue) {
if (key == Property.AXIS_CONVERSION) {
setAxisConversion((LinearAxisConversion) newValue);
}
throw new IllegalArgumentException("Invalid key: " + key);
}
public void setAxisConversion(LinearAxisConversion axisConversion) {
if (axisConversion == null) {
throw new IllegalArgumentException("axisConversion must be non-null");
}
this.axisConversion = axisConversion;
changeSupport.firePropertyChange(this, Property.AXIS_CONVERSION, null, axisConversion);
}
public void setLabelPosition(LabelPosition labelPosition) {
switch (labelPosition) {
case TOP:
case BOTTOM:
if (orientation != Orientation.HORIZONTAL) {
throw new IllegalArgumentException();
}
break;
case LEFT:
case RIGHT:
if (orientation != Orientation.VERTICAL) {
throw new IllegalArgumentException();
}
break;
}
this.labelPosition = labelPosition;
}
public void setMajorTickColor(Color majorTickColor) {
if (majorTickColor == null) {
throw new IllegalArgumentException("majorTickColor must be non-null");
}
this.majorTickColor = majorTickColor;
}
public void setMajorTickSize(int majorTickSize) {
this.majorTickSize = majorTickSize;
}
public void setMinMinorGridLineSpacing(int minMinorGridLineSpacing) {
this.minMinorGridLineSpacing = minMinorGridLineSpacing;
}
public void setMinorTickColor(Color minorTickColor) {
if (minorTickColor == null) {
throw new IllegalArgumentException("minorTickColor must be non-null");
}
this.minorTickColor = minorTickColor;
}
public void setMinorTickSize(int minorTickSize) {
this.minorTickSize = minorTickSize;
}
public void setTextPadding(int textPadding) {
labelPadding = textPadding;
}
public boolean updateSizes(Graphics2D g2) {
Insets insets = getInsets();
Dimension newMinSize;
Dimension newPrefSize;
if (orientation == Orientation.VERTICAL) {
double minorSpacing = GridMath
.niceCeiling(Math.abs(axisConversion.invert(minMinorGridLineSpacing) - axisConversion.invert(0)));
double majorSpacing = minorSpacing * 2;
double topValue = axisConversion.invert(0);
double bottomValue = axisConversion.invert(getHeight());
int fractionDigits = GridMath.niceCeilingFractionDigits(majorSpacing);
format.setMinimumFractionDigits(fractionDigits);
format.setMaximumFractionDigits(fractionDigits);
int labelWidth = (int) Math
.ceil(PlotUtils.calcHorizontalGridLineLabelsWidth(g2, topValue, bottomValue, majorSpacing, format));
int width = labelWidth + majorTickSize + labelPadding + insets.left + insets.right;
newMinSize = new Dimension(width, 0);
newPrefSize = new Dimension(width, 100);
} else {
int height = g2.getFontMetrics().getAscent() + majorTickSize + labelPadding + insets.top + insets.bottom;
newMinSize = new Dimension(0, height);
newPrefSize = new Dimension(100, height);
}
boolean changed = !getMinimumSize().equals(newPrefSize) || !getPreferredSize().equals(newPrefSize);
calcMinSize = newMinSize;
calcPrefSize = newPrefSize;
return changed;
}
}