From 067562283cbe27a913e8e133c725f783e310b2b1 Mon Sep 17 00:00:00 2001 From: yblanken Date: Tue, 25 May 2021 15:59:38 +0200 Subject: [PATCH] Adding some reusable annotations --- .../annotations/XYDataRangeAnnotation.java | 124 +++++ .../XYInversePointerAnnotation.java | 261 ++++++++++ .../chart/annotations/XYNoteAnnotation.java | 462 ++++++++++++++++++ .../java/org/jfree/chart/geom/GeomUtil.java | 158 ++++++ 4 files changed, 1005 insertions(+) create mode 100644 src/main/java/org/jfree/chart/annotations/XYDataRangeAnnotation.java create mode 100644 src/main/java/org/jfree/chart/annotations/XYInversePointerAnnotation.java create mode 100644 src/main/java/org/jfree/chart/annotations/XYNoteAnnotation.java create mode 100644 src/main/java/org/jfree/chart/geom/GeomUtil.java diff --git a/src/main/java/org/jfree/chart/annotations/XYDataRangeAnnotation.java b/src/main/java/org/jfree/chart/annotations/XYDataRangeAnnotation.java new file mode 100644 index 0000000000..06bccd0016 --- /dev/null +++ b/src/main/java/org/jfree/chart/annotations/XYDataRangeAnnotation.java @@ -0,0 +1,124 @@ +/* =========================================================== + * JFreeChart : a free chart library for the Java(tm) platform + * =========================================================== + * + * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jfreechart/index.html + * + * This library 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 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners.] + * + * ------------------------ + * XYDataRangeAnnotation.java + * ------------------------ + * (C) Copyright 2003-2021, by Object Refinery Limited. + * + * Original Author: Yuri Blankenstein (for ESI TNO); + * + */ +package org.jfree.chart.annotations; + +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.data.Range; + +/** + * This annotation can be put on an {@link XYPlot} to ensure a visible data + * range. The range values should be specified w.r.t. to the first (index 0) + * domain or range axis. + * + * @see XYPlot#getDataRange(ValueAxis) + * @see XYPlot#getDomainAxis() + * @see XYPlot#getRangeAxis() + */ +public class XYDataRangeAnnotation extends AbstractXYAnnotation implements XYAnnotationBoundsInfo { + private static final long serialVersionUID = 2058170262687146829L; + + private final Range minimumDomainRange; + private final Range minimumRangeRange; + + /** + * Creates a new instance. + * + * @param minimumDomainRange the range to ensure on the domain axis + * ({@code null} permitted). + * @param minimumRangeRange the range to ensure on the range axis + * ({@code null} permitted). + */ + public XYDataRangeAnnotation(Range minimumDomainRange, Range minimumRangeRange) { + this.minimumDomainRange = minimumDomainRange; + this.minimumRangeRange = minimumRangeRange; + } + + @Override + public boolean getIncludeInDataBounds() { + return true; + } + + @Override + public Range getXRange() { + return minimumDomainRange; + } + + @Override + public Range getYRange() { + return minimumRangeRange; + } + + @Override + public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea, ValueAxis domainAxis, ValueAxis rangeAxis, + int rendererIndex, PlotRenderingInfo info) { + // Nothing to do here + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((minimumDomainRange == null) ? 0 : minimumDomainRange.hashCode()); + result = prime * result + ((minimumRangeRange == null) ? 0 : minimumRangeRange.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + XYDataRangeAnnotation other = (XYDataRangeAnnotation) obj; + if (minimumDomainRange == null) { + if (other.minimumDomainRange != null) + return false; + } else if (!minimumDomainRange.equals(other.minimumDomainRange)) + return false; + if (minimumRangeRange == null) { + if (other.minimumRangeRange != null) + return false; + } else if (!minimumRangeRange.equals(other.minimumRangeRange)) + return false; + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/jfree/chart/annotations/XYInversePointerAnnotation.java b/src/main/java/org/jfree/chart/annotations/XYInversePointerAnnotation.java new file mode 100644 index 0000000000..e8d7fe087e --- /dev/null +++ b/src/main/java/org/jfree/chart/annotations/XYInversePointerAnnotation.java @@ -0,0 +1,261 @@ +/* =========================================================== + * JFreeChart : a free chart library for the Java(tm) platform + * =========================================================== + * + * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jfreechart/index.html + * + * This library 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 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners.] + * + * ------------------------ + * XYInversePointerAnnotation.java + * ------------------------ + * (C) Copyright 2003-2021, by Object Refinery Limited. + * + * Original Author: Yuri Blankenstein (for ESI TNO); + * + */ +package org.jfree.chart.annotations; + +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.Serializable; + +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.geom.GeomUtil; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.text.TextUtils; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.chart.util.PublicCloneable; + +/** + * An arrow and label that can be placed on an {@link XYPlot}. The arrow is + * drawn at a user-definable angle but points towards the label of the + * annotation. + *

+ * The arrow length (and its offset from the (x, y) location) is controlled by + * the tip radius and the base radius attributes. Imagine two circles around the + * (x, y) coordinate: the inner circle defined by the tip radius, and the outer + * circle defined by the base radius. Now, draw the arrow starting at some point + * on the outer circle (the point is determined by the angle), with the arrow + * tip being drawn at a corresponding point on the inner circle. + */ +public class XYInversePointerAnnotation extends XYPointerAnnotation + implements Cloneable, PublicCloneable, Serializable { + + /** For serialization. */ + private static final long serialVersionUID = -4031161445009858551L; + + /** The default dot radius (in Java2D units). */ + public static final double DEFAULT_DOT_RADIUS = 0; + + /** The radius of the dot at the start of the arrow. */ + private double dotRadius; + + /** + * Creates a new label and arrow annotation. + * + * @param label the label ({@code null} permitted). + * @param x the x-coordinate (measured against the chart's domain axis). + * @param y the y-coordinate (measured against the chart's range axis). + * @param angle the angle of the arrow's line (in radians). + */ + public XYInversePointerAnnotation(String label, double x, double y, + double angle) { + super(label, x, y, angle); + this.dotRadius = DEFAULT_DOT_RADIUS; + } + + /** + * Returns the radius of the dot at the start of the arrow. + * + * @return the radius of the dot at the start of the arrow + * @see #setDotRadius(double) + */ + public double getDotRadius() { + return dotRadius; + } + + /** + * Sets the radius of the dot at the start of the arrow, ≤ 0 will omit + * the dot. + * + * @param dotRadius the radius of the dot at the start of the arrow + * @see #getDotRadius() + */ + public void setDotRadius(double dotRadius) { + this.dotRadius = dotRadius; + fireAnnotationChanged(); + } + + /** + * Draws the annotation. + * + * @param g2 the graphics device. + * @param plot the plot. + * @param dataArea the data area. + * @param domainAxis the domain axis. + * @param rangeAxis the range axis. + * @param rendererIndex the renderer index. + * @param info the plot rendering info. + */ + @Override + public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea, + ValueAxis domainAxis, ValueAxis rangeAxis, int rendererIndex, + PlotRenderingInfo info) { + + PlotOrientation orientation = plot.getOrientation(); + RectangleEdge domainEdge = Plot.resolveDomainAxisLocation( + plot.getDomainAxisLocation(), orientation); + RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation( + plot.getRangeAxisLocation(), orientation); + double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge); + double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge); + if (orientation == PlotOrientation.HORIZONTAL) { + double temp = j2DX; + j2DX = j2DY; + j2DY = temp; + } + + double startX = j2DX + Math.cos(getAngle()) * getBaseRadius(); + double startY = j2DY + Math.sin(getAngle()) * getBaseRadius(); + + double endX = j2DX + Math.cos(getAngle()) * getTipRadius(); + double endY = j2DY + Math.sin(getAngle()) * getTipRadius(); + + // Already calculate the label bounds to adjust the start point + double labelX = j2DX + + Math.cos(getAngle()) * (getBaseRadius() + getLabelOffset()); + double labelY = j2DY + + Math.sin(getAngle()) * (getBaseRadius() + getLabelOffset()); + Shape hotspot = TextUtils.calculateRotatedStringBounds(getText(), g2, + (float) labelX, (float) labelY, getTextAnchor(), + getRotationAngle(), getRotationAnchor()); + + Point2D[] pointOnLabelBounds = GeomUtil.calculateIntersectionPoints( + new Line2D.Double(startX, startY, endX, endY), + GeomUtil.getLines(hotspot, null)); + if (pointOnLabelBounds.length > 0) { + // Adjust the start point to the intersection with the label bounds + startX = pointOnLabelBounds[0].getX(); + startY = pointOnLabelBounds[0].getY(); + } + + double arrowBaseX = startX - Math.cos(getAngle()) * getArrowLength(); + double arrowBaseY = startY - Math.sin(getAngle()) * getArrowLength(); + + double arrowLeftX = arrowBaseX + + Math.cos(getAngle() + Math.PI / 2.0) * getArrowWidth(); + double arrowLeftY = arrowBaseY + + Math.sin(getAngle() + Math.PI / 2.0) * getArrowWidth(); + + double arrowRightX = arrowBaseX + - Math.cos(getAngle() + Math.PI / 2.0) * getArrowWidth(); + double arrowRightY = arrowBaseY + - Math.sin(getAngle() + Math.PI / 2.0) * getArrowWidth(); + + GeneralPath arrow = new GeneralPath(); + arrow.moveTo((float) startX, (float) startY); + arrow.lineTo((float) arrowLeftX, (float) arrowLeftY); + arrow.lineTo((float) arrowRightX, (float) arrowRightY); + arrow.closePath(); + + g2.setStroke(getArrowStroke()); + g2.setPaint(getArrowPaint()); + Line2D line = new Line2D.Double(arrowBaseX, arrowBaseY, endX, endY); + g2.draw(line); + g2.fill(arrow); + + if (dotRadius > 0) { + Ellipse2D.Double dot = new Ellipse2D.Double(endX - dotRadius, + endY - dotRadius, 2 * dotRadius, 2 * dotRadius); + g2.fill(dot); + } + + // draw the label + g2.setFont(getFont()); + if (getBackgroundPaint() != null) { + g2.setPaint(getBackgroundPaint()); + g2.fill(hotspot); + } + + g2.setPaint(getPaint()); + TextUtils.drawRotatedString(getText(), g2, (float) labelX, + (float) labelY, getTextAnchor(), getRotationAngle(), + getRotationAnchor()); + if (isOutlineVisible()) { + g2.setStroke(getOutlineStroke()); + g2.setPaint(getOutlinePaint()); + g2.draw(hotspot); + } + + String toolTip = getToolTipText(); + String url = getURL(); + if (toolTip != null || url != null) { + addEntity(info, hotspot, rendererIndex, toolTip, url); + } + + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(dotRadius); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + XYInversePointerAnnotation other = (XYInversePointerAnnotation) obj; + if (Double.doubleToLongBits(dotRadius) != Double + .doubleToLongBits(other.dotRadius)) + return false; + return true; + } + + /** + * Returns a clone of the annotation. + * + * @return A clone. + * + * @throws CloneNotSupportedException if the annotation can't be cloned. + */ + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/src/main/java/org/jfree/chart/annotations/XYNoteAnnotation.java b/src/main/java/org/jfree/chart/annotations/XYNoteAnnotation.java new file mode 100644 index 0000000000..55a2db0dc7 --- /dev/null +++ b/src/main/java/org/jfree/chart/annotations/XYNoteAnnotation.java @@ -0,0 +1,462 @@ +/* =========================================================== + * JFreeChart : a free chart library for the Java(tm) platform + * =========================================================== + * + * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jfreechart/index.html + * + * This library 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 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners.] + * + * ------------------------ + * XYNoteAnnotation.java + * ------------------------ + * (C) Copyright 2003-2021, by Object Refinery Limited. + * + * Original Author: David Gilbert (as XYPointerAnnotation.java; for Object Refinery Limited); + * Contributor(s): Yuri Blankenstein (copy to XYNoteAnnotation.java; for ESI TNO); + * + */ +package org.jfree.chart.annotations; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Objects; + +import org.jfree.chart.HashUtils; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.event.AnnotationChangeEvent; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.text.TextUtils; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.chart.util.Args; +import org.jfree.chart.util.PublicCloneable; +import org.jfree.chart.util.SerialUtils; + +/** + * An line and label that can be placed on an {@link XYPlot}. The line is + * drawn at a user-definable angle so that it points towards the (x, y) + * location for the annotation. + *

+ * The line length (and its offset from the (x, y) location) is controlled by + * the tip radius and the base radius attributes. Imagine two circles around + * the (x, y) coordinate: the inner circle defined by the tip radius, and the + * outer circle defined by the base radius. Now, draw the line starting at + * some point on the outer circle (the point is determined by the angle), with + * the line tip being drawn at a corresponding point on the inner circle. + */ +public class XYNoteAnnotation extends XYTextAnnotation + implements Cloneable, PublicCloneable, Serializable { + + /** For serialization. */ + private static final long serialVersionUID = -4031161445009858551L; + + /** The default tip radius (in Java2D units). */ + public static final double DEFAULT_TIP_RADIUS = 0.0; + + /** The default base radius (in Java2D units). */ + public static final double DEFAULT_BASE_RADIUS = 30.0; + + /** The default label offset (in Java2D units). */ + public static final double DEFAULT_LABEL_OFFSET = 3.0; + + /** The default line stroke. */ + public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, new float[] {2.0f}, 0.0f); + + /** The default line stroke. */ + public static final Paint DEFAULT_BACKGROUND_PAINT = new Color(255, 255, 203); + + /** The default line stroke. */ + public static final Paint DEFAULT_OUTLINE_PAINT = new Color(255, 204, 102); + + /** The angle of the line's line (in radians). */ + private double angle; + + /** + * The radius from the (x, y) point to the tip of the line (in Java2D + * units). + */ + private double tipRadius; + + /** + * The radius from the (x, y) point to the start of the line line (in + * Java2D units). + */ + private double baseRadius; + + /** The line stroke. */ + private transient Stroke lineStroke; + + /** The line paint. */ + private transient Paint linePaint; + + /** The radius from the base point to the anchor point for the label. */ + private double labelOffset; + + /** + * Creates a new label and line annotation. + * + * @param label the label ({@code null} permitted). + * @param x the x-coordinate (measured against the chart's domain axis). + * @param y the y-coordinate (measured against the chart's range axis). + * @param angle the angle of the line's line (in radians). + */ + public XYNoteAnnotation(String label, double x, double y, double angle) { + super(label, x, y); + this.angle = angle; + this.tipRadius = DEFAULT_TIP_RADIUS; + this.baseRadius = DEFAULT_BASE_RADIUS; + this.labelOffset = DEFAULT_LABEL_OFFSET; + this.lineStroke = DEFAULT_LINE_STROKE; + this.linePaint = Color.BLACK; + + setBackgroundPaint(DEFAULT_BACKGROUND_PAINT); + setOutlineVisible(true); + setOutlinePaint(DEFAULT_OUTLINE_PAINT); + setOutlineStroke(new BasicStroke(1.0f)); + } + + /** + * Returns the angle of the line. + * + * @return The angle (in radians). + * + * @see #setAngle(double) + */ + public double getAngle() { + return this.angle; + } + + /** + * Sets the angle of the line and sends an + * {@link AnnotationChangeEvent} to all registered listeners. + * + * @param angle the angle (in radians). + * + * @see #getAngle() + */ + public void setAngle(double angle) { + this.angle = angle; + fireAnnotationChanged(); + } + + /** + * Returns the tip radius. + * + * @return The tip radius (in Java2D units). + * + * @see #setTipRadius(double) + */ + public double getTipRadius() { + return this.tipRadius; + } + + /** + * Sets the tip radius and sends an + * {@link AnnotationChangeEvent} to all registered listeners. + * + * @param radius the radius (in Java2D units). + * + * @see #getTipRadius() + */ + public void setTipRadius(double radius) { + this.tipRadius = radius; + fireAnnotationChanged(); + } + + /** + * Returns the base radius. + * + * @return The base radius (in Java2D units). + * + * @see #setBaseRadius(double) + */ + public double getBaseRadius() { + return this.baseRadius; + } + + /** + * Sets the base radius and sends an + * {@link AnnotationChangeEvent} to all registered listeners. + * + * @param radius the radius (in Java2D units). + * + * @see #getBaseRadius() + */ + public void setBaseRadius(double radius) { + this.baseRadius = radius; + fireAnnotationChanged(); + } + + /** + * Returns the label offset. + * + * @return The label offset (in Java2D units). + * + * @see #setLabelOffset(double) + */ + public double getLabelOffset() { + return this.labelOffset; + } + + /** + * Sets the label offset (from the line base, continuing in a straight + * line, in Java2D units) and sends an + * {@link AnnotationChangeEvent} to all registered listeners. + * + * @param offset the offset (in Java2D units). + * + * @see #getLabelOffset() + */ + public void setLabelOffset(double offset) { + this.labelOffset = offset; + fireAnnotationChanged(); + } + + /** + * Returns the stroke used to draw the line line. + * + * @return The line stroke (never {@code null}). + * + * @see #setLineStroke(Stroke) + */ + public Stroke getLineStroke() { + return this.lineStroke; + } + + /** + * Sets the stroke used to draw the line line and sends an + * {@link AnnotationChangeEvent} to all registered listeners. + * + * @param stroke the stroke ({@code null} not permitted). + * + * @see #getLineStroke() + */ + public void setLineStroke(Stroke stroke) { + Args.nullNotPermitted(stroke, "stroke"); + this.lineStroke = stroke; + fireAnnotationChanged(); + } + + /** + * Returns the paint used for the line. + * + * @return The line paint (never {@code null}). + * + * @see #setLinePaint(Paint) + */ + public Paint getLinePaint() { + return this.linePaint; + } + + /** + * Sets the paint used for the line and sends an + * {@link AnnotationChangeEvent} to all registered listeners. + * + * @param paint the line paint ({@code null} not permitted). + * + * @see #getLinePaint() + */ + public void setLinePaint(Paint paint) { + Args.nullNotPermitted(paint, "paint"); + this.linePaint = paint; + fireAnnotationChanged(); + } + + /** + * Draws the annotation. + * + * @param g2 the graphics device. + * @param plot the plot. + * @param dataArea the data area. + * @param domainAxis the domain axis. + * @param rangeAxis the range axis. + * @param rendererIndex the renderer index. + * @param info the plot rendering info. + */ + @Override + public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea, + ValueAxis domainAxis, ValueAxis rangeAxis, int rendererIndex, + PlotRenderingInfo info) { + + PlotOrientation orientation = plot.getOrientation(); + RectangleEdge domainEdge = Plot.resolveDomainAxisLocation( + plot.getDomainAxisLocation(), orientation); + RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation( + plot.getRangeAxisLocation(), orientation); + double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge); + double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge); + if (orientation == PlotOrientation.HORIZONTAL) { + double temp = j2DX; + j2DX = j2DY; + j2DY = temp; + } + double startX = j2DX + Math.cos(this.angle) * this.baseRadius; + double startY = j2DY + Math.sin(this.angle) * this.baseRadius; + + double endX = j2DX + Math.cos(this.angle) * this.tipRadius; + double endY = j2DY + Math.sin(this.angle) * this.tipRadius; + + g2.setStroke(this.lineStroke); + g2.setPaint(this.linePaint); + Line2D line = new Line2D.Double(startX, startY, endX, endY); + g2.draw(line); + + // draw the label + double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius + + this.labelOffset); + double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius + + this.labelOffset); + g2.setFont(getFont()); + Shape hotspot = TextUtils.calculateRotatedStringBounds( + getText(), g2, (float) labelX, (float) labelY, getTextAnchor(), + getRotationAngle(), getRotationAnchor()); + if (getBackgroundPaint() != null) { + g2.setPaint(getBackgroundPaint()); + g2.fill(hotspot); + } + g2.setPaint(getPaint()); + TextUtils.drawRotatedString(getText(), g2, (float) labelX, + (float) labelY, getTextAnchor(), getRotationAngle(), + getRotationAnchor()); + if (isOutlineVisible()) { + g2.setStroke(getOutlineStroke()); + g2.setPaint(getOutlinePaint()); + g2.draw(hotspot); + } + + String toolTip = getToolTipText(); + String url = getURL(); + if (toolTip != null || url != null) { + addEntity(info, hotspot, rendererIndex, toolTip, url); + } + + } + + /** + * Tests this annotation for equality with an arbitrary object. + * + * @param obj the object ({@code null} permitted). + * + * @return {@code true} or {@code false}. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof XYNoteAnnotation)) { + return false; + } + XYNoteAnnotation that = (XYNoteAnnotation) obj; + if (this.angle != that.angle) { + return false; + } + if (this.tipRadius != that.tipRadius) { + return false; + } + if (this.baseRadius != that.baseRadius) { + return false; + } + if (!this.linePaint.equals(that.linePaint)) { + return false; + } + if (!Objects.equals(this.lineStroke, that.lineStroke)) { + return false; + } + if (this.labelOffset != that.labelOffset) { + return false; + } + return super.equals(obj); + } + + /** + * Returns a hash code for this instance. + * + * @return A hash code. + */ + @Override + public int hashCode() { + int result = super.hashCode(); + long temp = Double.doubleToLongBits(this.angle); + result = 37 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(this.tipRadius); + result = 37 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(this.baseRadius); + result = 37 * result + (int) (temp ^ (temp >>> 32)); + result = result * 37 + HashUtils.hashCodeForPaint(this.linePaint); + result = result * 37 + this.lineStroke.hashCode(); + temp = Double.doubleToLongBits(this.labelOffset); + result = 37 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + /** + * Returns a clone of the annotation. + * + * @return A clone. + * + * @throws CloneNotSupportedException if the annotation can't be cloned. + */ + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + /** + * Provides serialization support. + * + * @param stream the output stream. + * + * @throws IOException if there is an I/O error. + */ + private void writeObject(ObjectOutputStream stream) throws IOException { + stream.defaultWriteObject(); + SerialUtils.writePaint(this.linePaint, stream); + SerialUtils.writeStroke(this.lineStroke, stream); + } + + /** + * Provides serialization support. + * + * @param stream the input stream. + * + * @throws IOException if there is an I/O error. + * @throws ClassNotFoundException if there is a classpath problem. + */ + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException { + stream.defaultReadObject(); + this.linePaint = SerialUtils.readPaint(stream); + this.lineStroke = SerialUtils.readStroke(stream); + } +} diff --git a/src/main/java/org/jfree/chart/geom/GeomUtil.java b/src/main/java/org/jfree/chart/geom/GeomUtil.java new file mode 100644 index 0000000000..93fca55aca --- /dev/null +++ b/src/main/java/org/jfree/chart/geom/GeomUtil.java @@ -0,0 +1,158 @@ +/* =========================================================== + * JFreeChart : a free chart library for the Java(tm) platform + * =========================================================== + * + * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jfreechart/index.html + * + * This library 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 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners.] + * + * ------------------------ + * XYDataRangeAnnotation.java + * ------------------------ + * (C) Copyright 2003-2021, by Object Refinery Limited. + * + * Original Author: Yuri Blankenstein (for ESI TNO); + * + */ +package org.jfree.chart.geom; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Line2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.util.ArrayList; + +/** + * Some utility methods for working with geometry in Java2D. + */ +public final class GeomUtil { + private GeomUtil() { + // Empty for utility classes + } + + /** + * For each line in {@code lines}, calculates its intersection point with + * {@code lineA}, possibly no intersection point exists (i.e. parallel + * lines). + * + * @param lineA line to calculate the intersection point for. + * @param lines lines to calculate the intersection points with. + * @return all intersections points between {@code lineA} and {@code lines}. + * @see #calculateIntersectionPoint(Line2D, Line2D) + */ + public static final Point2D[] calculateIntersectionPoints(Line2D lineA, + Line2D... lines) { + ArrayList intersectionPoints = new ArrayList( + lines.length); + for (Line2D lineB : lines) { + if (lineA.intersectsLine(lineB)) { + // Why does Java have the tester method, but not the method to + // get the point itself :S + intersectionPoints.add(calculateIntersectionPoint(lineA, lineB)); + } + } + return intersectionPoints.toArray(new Point2D[intersectionPoints.size()]); + } + + /** + * Calculates the intersection point of {@code lineA} with {@code lineB}, + * possibly {@code null} if no intersection point exists (i.e. parallel + * lines). + * + * @param lineA the first line for the calculation + * @param lineB the second line for the calculation + * @return the intersection point of {@code lineA} with {@code lineB}, + * possibly {@code null} if no intersection point exists + */ + public static final Point2D calculateIntersectionPoint(Line2D lineA, + Line2D lineB) { + double x1 = lineA.getX1(); + double y1 = lineA.getY1(); + double x2 = lineA.getX2(); + double y2 = lineA.getY2(); + + double x3 = lineB.getX1(); + double y3 = lineB.getY1(); + double x4 = lineB.getX2(); + double y4 = lineB.getY2(); + + Point2D p = null; + + double d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (d != 0) { + double xi = ((x3 - x4) * (x1 * y2 - y1 * x2) + - (x1 - x2) * (x3 * y4 - y3 * x4)) / d; + double yi = ((y3 - y4) * (x1 * y2 - y1 * x2) + - (y1 - y2) * (x3 * y4 - y3 * x4)) / d; + + p = new Point2D.Double(xi, yi); + } + return p; + } + + /** + * Returns all {@link PathIterator#SEG_LINETO line segments} building up a + * {@code shape}. + * + * @param shape a shape that is built up of {@link PathIterator#SEG_LINETO} + * elements. + * @param at an optional {@code AffineTransform} to be applied to the + * coordinates as they are returned in the iteration, or + * {@code null} if untransformed coordinates are desired + * @return all {@link PathIterator#SEG_LINETO line segments} building up the + * {@code shape} + * @throws IllegalArgumentException if {@code shape} contains non-straight + * line segments (i.e. + * {@link PathIterator#SEG_CUBICTO} or + * {@link PathIterator#SEG_QUADTO}) + */ + public static final Line2D[] getLines(Shape shape, AffineTransform at) + throws IllegalArgumentException { + ArrayList lines = new ArrayList(); + Point2D first = null; + Point2D current = null; + double[] coords = new double[6]; + for (PathIterator pathIterator = shape.getPathIterator(at); + !pathIterator.isDone(); pathIterator.next()) { + switch (pathIterator.currentSegment(coords)) { + case PathIterator.SEG_MOVETO: + current = new Point2D.Double(coords[0], coords[1]); + break; + case PathIterator.SEG_LINETO: + Point2D to = new Point2D.Double(coords[0], coords[1]); + lines.add(new Line2D.Double(current, to)); + current = to; + break; + case PathIterator.SEG_CLOSE: + lines.add(new Line2D.Double(current, first)); + current = first; + break; + default: + throw new IllegalArgumentException( + "Shape contains non-straight line segments"); + } + if (null == first) + first = current; + } + return lines.toArray(new Line2D[lines.size()]); + } +}