/**
* Copyright 2010-2014 Jingjing Li.
*
* This file is part of jplot2d.
*
* jplot2d is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* jplot2d 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 Lesser Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with jplot2d. If not, see .
*/
package org.jplot2d.element.impl;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jplot2d.element.AxisLabelSide;
import org.jplot2d.element.AxisOrientation;
import org.jplot2d.element.AxisPosition;
import org.jplot2d.element.AxisTickManager;
import org.jplot2d.element.AxisTickSide;
import org.jplot2d.element.Element;
import org.jplot2d.element.HAlign;
import org.jplot2d.element.Plot;
import org.jplot2d.element.VAlign;
import org.jplot2d.tex.MathElement;
import org.jplot2d.tex.MathLabel;
import org.jplot2d.transform.PaperTransform;
import org.jplot2d.util.DoubleDimension2D;
/**
* @author Jingjing Li
*
*/
public class AxisImpl extends ComponentImpl implements AxisEx {
/**
* the default width, 1.0 pt.
*/
private static final float DEFAULT_AXISLINE_WIDTH = 1.0f;
/* the gap between label and tick */
private static final double LABEL_GAP_RATIO = 1.0 / 4;
/* the gap between title and label */
private static final double TITLE_GAP_RATIO = 1.0 / 4;
private final AxisTitleEx title;
private AxisTickManagerEx tickManager;
private double locX, locY;
private AxisOrientation orientation;
private double length;
private AxisPosition position = AxisPosition.NEGATIVE_SIDE;
private float axisLineWidth = DEFAULT_AXISLINE_WIDTH;
private boolean showGridLines, showMinorGridLines;
private boolean tickVisible = true;
private AxisTickSide tickSide = AxisTickSide.INWARD;
private double tickHeight = 8.0;
private double minorHeight = 4.0;
private float tickLineWidth = DEFAULT_AXISLINE_WIDTH / 2;
private boolean labelVisible = true;
private AxisOrientation labelOrientation = AxisOrientation.HORIZONTAL;
private AxisLabelSide labelSide = AxisLabelSide.OUTWARD;
private Color labelColor;
/* thickness */
private double asc, desc;
private double labelOffset;
private double labelRotation;
private HAlign labelHAlign;
private VAlign labelVAlign;
private double titleOffset;
private VAlign titleVAlign;
private boolean thicknessCalculationNeeded = true;
private Font actualLabelFont;
public AxisImpl() {
setSelectable(true);
title = new AxisTitleImpl();
title.setParent(this);
}
protected AxisImpl(AxisTitleEx title) {
this.title = title;
}
public String getId() {
if (getParent() != null) {
switch (getOrientation()) {
case HORIZONTAL:
int xidx = getParent().indexOfXAxis(this);
return "X" + xidx;
case VERTICAL:
int yidx = getParent().indexOfYAxis(this);
return "Y" + yidx;
}
}
return "Axis@" + Integer.toHexString(System.identityHashCode(this));
}
public String getShortId() {
if (getParent() != null) {
String pid = getParent().getShortId();
if (pid == null) {
return getId();
} else {
return getId() + "." + pid;
}
} else {
return getId();
}
}
public InvokeStep getInvokeStepFormParent() {
if (parent == null) {
return null;
}
try {
switch (getOrientation()) {
case HORIZONTAL:
Method xmethod = Plot.class.getMethod("getXAxis", Integer.TYPE);
return new InvokeStep(xmethod, getParent().indexOfXAxis(this));
case VERTICAL:
Method ymethod = Plot.class.getMethod("getYAxis", Integer.TYPE);
return new InvokeStep(ymethod, getParent().indexOfYAxis(this));
default:
return null;
}
} catch (NoSuchMethodException e) {
throw new Error(e);
}
}
public PlotEx getParent() {
return (PlotEx) super.getParent();
}
public Map getMooringMap() {
Map result = new HashMap();
if (tickManager.getParent() == this) {
AxisTransformEx rman = tickManager.getAxisTransform();
if (rman.getParent() == tickManager) {
for (LayerEx layer : rman.getLayers()) {
result.put(rman, layer);
}
}
}
return result;
}
public void thisEffectiveColorChanged() {
redraw(this);
}
public void thisEffectiveFontChanged() {
invalidateThickness();
redraw(this);
}
public Point2D getLocation() {
return new Point2D.Double(locX, locY);
}
public void setLocation(double locX, double locY) {
if (getLocation().getX() != locX || getLocation().getY() != locY) {
this.locX = locX;
this.locY = locY;
redraw(this);
}
}
public AxisOrientation getOrientation() {
return orientation;
}
public void setOrientation(AxisOrientation orientation) {
this.orientation = orientation;
}
public Dimension2D getSize() {
if (getOrientation() == null) {
return null;
} else {
switch (getOrientation()) {
case HORIZONTAL:
return new DoubleDimension2D(getLength(), getThickness());
case VERTICAL:
return new DoubleDimension2D(getThickness(), getLength());
default:
return null;
}
}
}
public Rectangle2D getBounds() {
if (getParent() == null) {
return null;
} else {
return new Rectangle2D.Double(0, -desc, getLength(), getThickness());
}
}
public PaperTransform getPaperTransform() {
PaperTransform pxf = super.getPaperTransform();
if (pxf == null) {
return null;
} else {
if (getOrientation() == AxisOrientation.VERTICAL) {
pxf = pxf.rotate(Math.PI / 2);
}
return pxf;
}
}
public void setVisible(boolean visible) {
super.setVisible(visible);
invalidatePlot();
}
/**
* Invalidate the parent plot.
*/
private void invalidatePlot() {
if (getParent() != null) {
getParent().invalidate();
}
}
public AxisTickManagerEx getTickManager() {
return tickManager;
}
public void setTickManager(AxisTickManager tickManager) {
if (this.tickManager != null) {
this.tickManager.removeAxis(this);
}
this.tickManager = (AxisTickManagerEx) tickManager;
if (this.tickManager != null) {
this.tickManager.addAxis(this);
}
}
public double getLength() {
return length;
}
public void setLength(double length) {
if (this.length != length) {
this.length = length;
if (tickManager.getAxisTransform().getLockGroup().isAutoRange()) {
tickManager.getAxisTransform().getLockGroup().reAutoRange();
}
}
}
public AxisPosition getPosition() {
return position;
}
public void setPosition(AxisPosition position) {
this.position = position;
invalidateThickness();
invalidatePlot();
}
public float getAxisLineWidth() {
return axisLineWidth;
}
public void setAxisLineWidth(float width) {
this.axisLineWidth = width;
redraw(this);
}
public boolean isGridLines() {
return showGridLines;
}
public void setGridLines(boolean showGridLines) {
this.showGridLines = showGridLines;
redraw(this);
}
public boolean isMinorGridLines() {
return showMinorGridLines;
}
public void setMinorGridLines(boolean showGridLines) {
this.showMinorGridLines = showGridLines;
redraw(this);
}
public boolean isTickVisible() {
return tickVisible;
}
public void setTickVisible(boolean visible) {
this.tickVisible = visible;
invalidateThickness();
redraw(this);
}
public AxisTickSide getTickSide() {
return tickSide;
}
public void setTickSide(AxisTickSide side) {
this.tickSide = side;
invalidateThickness();
redraw(this);
}
public double getTickHeight() {
return tickHeight;
}
public void setTickHeight(double height) {
this.tickHeight = height;
invalidateThickness();
redraw(this);
}
public double getMinorTickHeight() {
return minorHeight;
}
public void setMinorTickHeight(double height) {
this.minorHeight = height;
}
public float getTickLineWidth() {
return tickLineWidth;
}
public void setTickLineWidth(float width) {
this.tickLineWidth = width;
redraw(this);
}
public boolean isLabelVisible() {
return labelVisible;
}
public void setLabelVisible(boolean visible) {
this.labelVisible = visible;
invalidateThickness();
redraw(this);
}
public AxisOrientation getLabelOrientation() {
return labelOrientation;
}
public void setLabelOrientation(AxisOrientation orientation) {
this.labelOrientation = orientation;
invalidateThickness();
redraw(this);
}
private boolean isLabelSameOrientation() {
return getOrientation() == getLabelOrientation();
}
public AxisLabelSide getLabelSide() {
return labelSide;
}
public void setLabelSide(AxisLabelSide side) {
this.labelSide = side;
invalidateThickness();
redraw(this);
}
public Color getLabelColor() {
return labelColor;
}
public void setLabelColor(Color color) {
this.labelColor = color;
redraw(this);
}
private Font getActualLabelFont() {
if (actualLabelFont != null) {
return actualLabelFont;
} else {
return getEffectiveFont();
}
}
public void setActualFont(Font font) {
if (!font.equals(this.actualLabelFont)) {
this.actualLabelFont = font;
redraw(this);
}
}
public AxisTitleEx getTitle() {
return title;
}
public double getThickness() {
return asc + desc;
}
public double getAsc() {
return asc;
}
/**
* always negative
*/
public double getDesc() {
return desc;
}
/**
* Mark the thickness is invalid. Changing tick height, label strings label font or orientation will call this
* method
*/
public void invalidateThickness() {
thicknessCalculationNeeded = true;
}
public void calcThickness() {
if (!thicknessCalculationNeeded) {
return;
}
thicknessCalculationNeeded = false;
asc = 0;
desc = 0;
labelOffset = 0;
labelVAlign = null;
labelHAlign = null;
titleOffset = 0;
titleVAlign = null;
if (isTickVisible()) {
if (isTickBothSide() || isTickAscSide()) {
asc += getTickHeight();
}
if (isTickBothSide() || !isTickAscSide()) {
desc += getTickHeight();
}
}
if (isLabelVisible()) {
double labelHeight = getLabelHeight();
if (isLabelAscSide()) {
labelOffset = asc + labelHeight * LABEL_GAP_RATIO;
if (isLabelSameOrientation()) {
asc = labelOffset + labelHeight;
} else {
asc = labelOffset + getLabelsMaxPaperWidth();
}
} else {
labelOffset = -desc - labelHeight * LABEL_GAP_RATIO;
if (isLabelSameOrientation()) {
desc = -labelOffset + labelHeight;
} else {
desc = -labelOffset + getLabelsMaxPaperWidth();
}
}
if (isLabelSameOrientation()) {
labelRotation = 0;
labelHAlign = HAlign.CENTER;
if (isLabelAscSide()) {
labelVAlign = VAlign.BOTTOM;
} else {
labelVAlign = VAlign.TOP;
}
} else {
if (getOrientation() == AxisOrientation.HORIZONTAL) {
labelRotation = Math.PI / 2.0;
} else {
labelRotation = -Math.PI / 2.0;
}
labelVAlign = VAlign.MIDDLE;
boolean axisPositiveSide = (getPosition() == AxisPosition.POSITIVE_SIDE);
boolean outwardSide = (getLabelSide() == AxisLabelSide.OUTWARD);
if (axisPositiveSide == outwardSide) {
labelHAlign = HAlign.LEFT;
} else {
labelHAlign = HAlign.RIGHT;
}
}
}
if (getTitle().isVisible() && getTitle().getTextModel() != null) {
if (isTitleAscSide()) {
titleVAlign = VAlign.BOTTOM;
getTitle().setVAlign(titleVAlign);
double titleHeight = getTitle().getSize().getHeight();
titleOffset = asc + titleHeight * TITLE_GAP_RATIO;
asc = titleOffset + titleHeight;
} else {
titleVAlign = VAlign.TOP;
getTitle().setVAlign(titleVAlign);
double titleHeight = getTitle().getSize().getHeight();
titleOffset = -desc - titleHeight * TITLE_GAP_RATIO;
desc = -titleOffset + titleHeight;
}
}
}
private boolean isTitleAscSide() {
boolean axisHorizontal = (getOrientation() == AxisOrientation.HORIZONTAL);
boolean axisPositiveSide = (getPosition() == AxisPosition.POSITIVE_SIDE);
return axisHorizontal == axisPositiveSide;
}
private boolean isTickBothSide() {
return getTickSide() == AxisTickSide.BOTH;
}
private boolean isTickAscSide() {
boolean axisHorizontal = (getOrientation() == AxisOrientation.HORIZONTAL);
boolean axisPositiveSide = (getPosition() == AxisPosition.POSITIVE_SIDE);
boolean outwardSide = (getTickSide() == AxisTickSide.OUTWARD);
return (axisHorizontal == axisPositiveSide) == outwardSide;
}
private boolean isLabelAscSide() {
boolean axisHorizontal = (getOrientation() == AxisOrientation.HORIZONTAL);
boolean axisPositiveSide = (getPosition() == AxisPosition.POSITIVE_SIDE);
boolean outwardSide = (getLabelSide() == AxisLabelSide.OUTWARD);
return (axisHorizontal == axisPositiveSide) == outwardSide;
}
/**
* Returns the height label by axis effective font.
*
* @return the label height
*/
private double getLabelHeight() {
FontRenderContext frc = new FontRenderContext(null, false, true);
LineMetrics lm = getEffectiveFont().getLineMetrics("Can be any string", frc);
return (lm.getAscent() + lm.getDescent());
}
/**
* Traverse all labels to find the Maximum Width. The width is calculated by axis effective font, not shrunk actual
* font.
*
* @return the maximum label width.
*/
private double getLabelsMaxPaperWidth() {
Dimension2D[] labelsSize = getLabelsPaperSize(getTickManager().getLabelModels(), getEffectiveFont());
double maxWidth = 0;
double maxHeight = 0;
for (int i = 0; i < labelsSize.length; i++) {
if (maxWidth < labelsSize[i].getWidth()) {
maxWidth = labelsSize[i].getWidth();
}
if (maxHeight < labelsSize[i].getHeight()) {
maxHeight = labelsSize[i].getHeight();
}
}
return maxWidth;
}
/**
* Returns the labels normal size.
*
* @return the labels normal size.
*/
public static Dimension2D[] getLabelsPaperSize(MathElement[] labels, Font labelFont) {
Dimension2D[] ss = new Dimension2D[labels.length];
for (int i = 0; i < labels.length; i++) {
MathLabel labelDrawer = new MathLabel(labels[i], labelFont, VAlign.MIDDLE, HAlign.CENTER);
Rectangle2D rect = labelDrawer.getBounds();
ss[i] = new DoubleDimension2D(rect.getWidth(), rect.getHeight());
}
return ss;
}
@Override
public ComponentEx copyStructure(Map orig2copyMap) {
AxisImpl result = new AxisImpl((AxisTitleEx) title.copyStructure(orig2copyMap));
result.title.setParent(result);
if (orig2copyMap != null) {
orig2copyMap.put(this, result);
}
// copy or link axis tick manager
AxisTickManagerEx atmCopy = (AxisTickManagerEx) orig2copyMap.get(tickManager);
if (atmCopy == null) {
atmCopy = (AxisTickManagerEx) tickManager.copyStructure(orig2copyMap);
}
result.tickManager = atmCopy;
atmCopy.addAxis(result);
return result;
}
@Override
public void copyFrom(ElementEx src) {
super.copyFrom(src);
AxisImpl axis = (AxisImpl) src;
this.locX = axis.locX;
this.locY = axis.locY;
this.orientation = axis.orientation;
this.length = axis.length;
this.position = axis.position;
this.axisLineWidth = axis.axisLineWidth;
this.showGridLines = axis.showGridLines;
this.showMinorGridLines = axis.showMinorGridLines;
this.tickVisible = axis.tickVisible;
this.tickSide = axis.tickSide;
this.tickHeight = axis.tickHeight;
this.minorHeight = axis.minorHeight;
this.tickLineWidth = axis.tickLineWidth;
this.labelVisible = axis.labelVisible;
this.labelOrientation = axis.labelOrientation;
this.labelSide = axis.labelSide;
this.labelColor = axis.labelColor;
this.asc = axis.asc;
this.desc = axis.desc;
this.labelOffset = axis.labelOffset;
this.labelRotation = axis.labelRotation;
this.labelHAlign = axis.labelHAlign;
this.labelVAlign = axis.labelVAlign;
this.titleOffset = axis.titleOffset;
this.titleVAlign = axis.titleVAlign;
this.actualLabelFont = axis.actualLabelFont;
}
public void draw(Graphics2D graphics) {
Graphics2D g = (Graphics2D) graphics.create();
g.transform(getPaperTransform().getTransform());
g.setColor(getEffectiveColor());
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setStroke(new BasicStroke(axisLineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
drawAxisLine(g);
g.setStroke(new BasicStroke(tickLineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
drawTicks(g);
if (isLabelVisible()) {
drawLabels(g, labelVAlign, labelHAlign);
}
if (getTitle().isVisible() && getTitle().getTextModel() != null) {
drawTitle(graphics);
}
g.dispose();
}
private void drawAxisLine(Graphics2D g2) {
Shape s = new Line2D.Double(0, 0, getLength(), 0);
g2.draw(s);
}
/**
* Draw ticks and grid lines.
*
* @param g
* Graphics
*/
private void drawTicks(Graphics2D g) {
Object tvs = getTickManager().getTickValues();
Object mvs = getTickManager().getMinorTickValues();
int tvslen = Array.getLength(tvs);
int mvslen = Array.getLength(mvs);
/*
* implement notes: grid lines must be drawn before ticks, otherwise the tick may be overlapped.
*/
if (showGridLines && tvslen > 0) {
Stroke oldStroke = g.getStroke();
Color oldColor = g.getColor();
Stroke gridLineStroke = new BasicStroke(((BasicStroke) oldStroke).getLineWidth() / 2, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 10.0f, new float[] { 2.0f, 2.0f }, 0.0f);
g.setStroke(gridLineStroke);
Color c = getEffectiveColor();
g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha() / 2));
Dimension2D pcontentSize = getParent().getContentSize();
AxisEx[] orthoAxes = getOrthoAxes();
for (int i = 0; i < tvslen; i++) {
double xp = transTickToPaper(Array.getDouble(tvs, i));
drawGridLine(g, xp, pcontentSize, orthoAxes);
}
g.setStroke(oldStroke);
g.setColor(oldColor);
}
if (showMinorGridLines && mvslen > 0) {
Stroke oldStroke = g.getStroke();
Color oldColor = g.getColor();
Stroke gridLineStroke = new BasicStroke(((BasicStroke) oldStroke).getLineWidth() / 4, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 10.0f, new float[] { 2.0f, 2.0f }, 0.0f);
g.setStroke(gridLineStroke);
Color c = getEffectiveColor();
g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha() / 4));
Dimension2D pcontentSize = getParent().getContentSize();
AxisEx[] orthoAxes = getOrthoAxes();
for (int i = 0; i < mvslen; i++) {
double xp = transTickToPaper(Array.getDouble(mvs, i));
drawGridLine(g, xp, pcontentSize, orthoAxes);
}
g.setStroke(oldStroke);
g.setColor(oldColor);
}
if (isTickVisible()) {
for (int i = 0; i < tvslen; i++) {
double xp = transTickToPaper(Array.getDouble(tvs, i));
drawTic(g, xp, getTickHeight());
}
for (int i = 0; i < mvslen; i++) {
double xp = transTickToPaper(Array.getDouble(mvs, i));
drawTic(g, xp, getMinorTickHeight());
}
}
}
/**
* Draw all labels of this axis.
*
* @param g
* @param vertalign
* @param horzalign
*/
private void drawLabels(Graphics2D g, VAlign vertalign, HAlign horzalign) {
Object tvs = getTickManager().getTickValues();
MathElement[] labels = getTickManager().getLabelModels();
Color color = getLabelColor();
g.setColor(color);
AffineTransform oldTransform = g.getTransform();
for (int i = 0; i < labels.length; i++) {
double x = Array.getDouble(tvs, i * getTickManager().getLabelInterval());
double xt = transTickToPaper(x);
MathLabel label = new MathLabel(labels[i], getActualLabelFont(), vertalign, horzalign);
g.translate(xt, labelOffset);
if (labelRotation != 0) {
g.rotate(labelRotation);
}
g.scale(1, -1);
label.draw(g);
g.setTransform(oldTransform);
}
}
private void drawTitle(Graphics2D g) {
double x = getLength() * 0.5;
getTitle().draw(g, x, titleOffset);
}
private void drawTic(Graphics2D g, double xp, double ticHeight) {
double yp0, yp1;
if (isTickBothSide()) {
yp0 = +ticHeight;
yp1 = -ticHeight;
} else if (isTickAscSide()) {
yp0 = +ticHeight;
yp1 = 0;
} else {
yp0 = 0;
yp1 = -ticHeight;
}
Shape line = new Line2D.Double(xp, yp0, xp, yp1);
g.draw(line);
}
/**
* Draw vertical grid line on X axis
*
* @param g
* @param xp
* x location
* @param contentSize
* @param orthoAxes
*/
private void drawGridLine(Graphics2D g, double xp, Dimension2D contentSize, AxisEx[] orthoAxes) {
if (getOrientation() == AxisOrientation.HORIZONTAL) {
/* avoid overlapping on the other Y axes */
for (AxisEx axis : orthoAxes) {
if (Math.abs(axis.getLocation().getX() - xp) < DEFAULT_AXISLINE_WIDTH) {
return;
}
}
if (position == AxisPosition.NEGATIVE_SIDE) {
Shape line = new Line2D.Double(xp, 0, xp, contentSize.getHeight());
g.draw(line);
} else {
Shape line = new Line2D.Double(xp, -contentSize.getHeight(), xp, 0);
g.draw(line);
}
} else {
/* avoid overlapping on the other X axes */
for (AxisEx axis : orthoAxes) {
if (Math.abs(axis.getLocation().getY() - xp) < DEFAULT_AXISLINE_WIDTH) {
return;
}
}
if (position == AxisPosition.NEGATIVE_SIDE) {
Shape line = new Line2D.Double(xp, -contentSize.getWidth(), xp, 0);
g.draw(line);
} else {
Shape line = new Line2D.Double(xp, 0, xp, contentSize.getWidth());
g.draw(line);
}
}
}
private AxisEx[] getOrthoAxes() {
List axes = new ArrayList();
if (getOrientation() == AxisOrientation.HORIZONTAL) {
for (AxisEx a : getParent().getYAxes()) {
axes.add(a);
}
} else {
for (AxisEx a : getParent().getXAxes()) {
axes.add(a);
}
}
return axes.toArray(new AxisEx[axes.size()]);
}
/**
* Transform tick value to paper value on this axis.
*
* @param the
* tick value
* @return paper value
*/
private double transTickToPaper(double tickValue) {
double uv;
if (tickManager.getTickTransform() != null) {
uv = tickManager.getTickTransform().transformTick2User(tickValue);
} else {
uv = tickValue;
}
return tickManager.getAxisTransform().getNormalTransform().convToNR(uv) * length;
}
}
Ads help maintain this website.