001/* =========================================================== 002 * Orson Charts : a 3D chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C)opyright 2013-2022, by David Gilbert. All rights reserved. 006 * 007 * https://github.com/jfree/orson-charts 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * Orson Charts home page: 028 * 029 * http://www.object-refinery.com/orsoncharts/index.html 030 * 031 */ 032 033package org.jfree.chart3d.marker; 034 035import java.awt.FontMetrics; 036import java.awt.Graphics2D; 037import java.awt.geom.Line2D; 038import java.awt.geom.Rectangle2D; 039import javax.swing.event.EventListenerList; 040import org.jfree.chart3d.Chart3DChangeListener; 041import org.jfree.chart3d.ChartElementVisitor; 042import org.jfree.chart3d.graphics2d.Anchor2D; 043import org.jfree.chart3d.graphics2d.RefPt2D; 044import org.jfree.chart3d.graphics2d.TextAnchor; 045import org.jfree.chart3d.graphics3d.internal.Utils2D; 046import org.jfree.chart3d.internal.Args; 047import org.jfree.chart3d.internal.TextUtils; 048 049/** 050 * A base class for implementing markers (includes the event notification 051 * mechanism). 052 * 053 * @since 1.2 054 */ 055public abstract class AbstractMarker implements Marker { 056 057 /** Storage for registered change listeners. */ 058 private final transient EventListenerList listenerList; 059 060 /** 061 * Default constructor. 062 */ 063 AbstractMarker() { 064 this.listenerList = new EventListenerList(); 065 } 066 067 /** 068 * Draws a marker label. 069 * 070 * @param g2 the graphics target ({@code null} not permitted). 071 * @param label the label. 072 * @param x the x-coordinate for the anchor point. 073 * @param y the y-cpordinate for the anchor point. 074 * @param anchor the label anchor ({@code null} not permitted). 075 * @param refLine a reference line that is used to determine the rotation 076 * angle for the label ({@code null} not permitted). 077 * @param reverse a flag to indicate reverse orientation. 078 */ 079 protected void drawMarkerLabel(Graphics2D g2, String label, 080 double x, double y, Anchor2D anchor, Line2D refLine, 081 boolean reverse) { 082 double angle = Utils2D.calculateTheta(refLine); 083 boolean vflip = false; 084 if (angle > Math.PI / 2) { 085 angle -= Math.PI; 086 vflip = true; 087 } 088 if (angle < -Math.PI / 2) { 089 angle += Math.PI; 090 vflip = true; 091 } 092 if (reverse) { 093 vflip = !vflip; 094 } 095 double lineLength = Utils2D.length(refLine); 096 FontMetrics fm = g2.getFontMetrics(); 097 Rectangle2D bounds = fm.getStringBounds(label, g2); 098 if (bounds.getWidth() < lineLength) { 099 TextAnchor textAnchor = deriveTextAnchorForLine(anchor.getRefPt(), 100 !vflip); 101 TextUtils.drawRotatedString(label, g2, 102 (float) x, (float) y, textAnchor, angle, textAnchor); 103 } 104 } 105 106 /** 107 * Draws a marker label. 108 * 109 * @param g2 the graphics target ({@code null} not permitted). 110 * @param label the label. 111 * @param x the x-coordinate for the anchor point. 112 * @param y the y-cpordinate for the anchor point. 113 * @param anchor the label anchor ({@code null} not permitted). 114 * @param refLine1 a reference line that is used to determine the rotation 115 * angle for the label ({@code null} not permitted). 116 * @param refLine2 a reference line that is used to determine the rotation 117 * angle for the label ({@code null} not permitted). 118 * @param reverse a flag to indicate reverse orientation. 119 */ 120 protected void drawMarkerLabel(Graphics2D g2, String label, 121 double x, double y, Anchor2D anchor, Line2D refLine1, 122 Line2D refLine2, boolean reverse) { 123 double angle; 124 if (anchor.getRefPt().isTop()) { 125 angle = Utils2D.calculateTheta(refLine2); 126 } else if (anchor.getRefPt().isBottom()) { 127 angle = Utils2D.calculateTheta(refLine1); 128 } else { 129 angle = (Utils2D.calculateTheta(refLine1) 130 + Utils2D.calculateTheta(refLine2)) / 2.0; 131 } 132 boolean vflip = false; 133 if (angle > Math.PI / 2) { 134 angle -= Math.PI; 135 vflip = true; 136 } 137 if (angle < -Math.PI / 2) { 138 angle += Math.PI; 139 vflip = true; 140 } 141 if (reverse) { 142 vflip = !vflip; 143 } 144 double lineLength1 = Utils2D.length(refLine1); 145 double lineLength2 = Utils2D.length(refLine2); 146 Rectangle2D bounds = g2.getFontMetrics().getStringBounds(label, g2); 147 if (bounds.getWidth() < Math.min(lineLength1, lineLength2)) { 148 TextAnchor textAnchor = deriveTextAnchor(anchor.getRefPt(), !vflip); 149 TextUtils.drawRotatedString(label, g2, (float) x, (float) y, 150 textAnchor, angle, textAnchor); 151 } 152 } 153 154 /** 155 * Receives a visitor. 156 * 157 * @param visitor the visitor. 158 * 159 * @since 1.2 160 */ 161 @Override 162 public void receive(ChartElementVisitor visitor) { 163 visitor.visit(this); 164 } 165 166 /** 167 * Registers a listener to receive notification of changes to the marker. 168 * 169 * @param listener the listener ({@code null} not permitted). 170 */ 171 @Override 172 public void addChangeListener(MarkerChangeListener listener) { 173 this.listenerList.add(MarkerChangeListener.class, listener); 174 } 175 176 /** 177 * Deregisters a listener so that it no longer receives notification of 178 * changes to the marker. 179 * 180 * @param listener the listener ({@code null} not permitted). 181 */ 182 @Override 183 public void removeChangeListener(MarkerChangeListener listener) { 184 this.listenerList.remove(MarkerChangeListener.class, listener); 185 } 186 187 /** 188 * Sends a {@link MarkerChangeEvent} to all registered listeners. 189 */ 190 protected void fireChangeEvent() { 191 Object[] listeners = this.listenerList.getListenerList(); 192 for (int i = listeners.length - 2; i >= 0; i -= 2) { 193 if (listeners[i] == Chart3DChangeListener.class) { 194 ((MarkerChangeListener) listeners[i + 1]).markerChanged( 195 new MarkerChangeEvent(this, this)); 196 } 197 } 198 } 199 200 /** 201 * A utility method that returns a suitable text anchor for a given 202 * reference point. This is used for range marker label positioning. 203 * 204 * @param refPt the reference point ({@code null} not permitted). 205 * @param vflip is the text flipped vertically? 206 * 207 * @return A text anchor (never {@code null}). 208 */ 209 protected static TextAnchor deriveTextAnchor(RefPt2D refPt, boolean vflip) { 210 Args.nullNotPermitted(refPt, "refPt"); 211 if (refPt.equals(RefPt2D.TOP_LEFT)) { 212 return vflip ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_RIGHT; 213 } else if (refPt.equals(RefPt2D.TOP_CENTER)) { 214 return vflip ? TextAnchor.TOP_CENTER : TextAnchor.BOTTOM_CENTER; 215 } else if (refPt.equals(RefPt2D.TOP_RIGHT)) { 216 return vflip ? TextAnchor.TOP_RIGHT :TextAnchor.BOTTOM_LEFT; 217 } if (refPt.equals(RefPt2D.CENTER_LEFT)) { 218 return vflip ? TextAnchor.CENTER_LEFT : TextAnchor.CENTER_RIGHT; 219 } else if (refPt.equals(RefPt2D.CENTER)) { 220 return TextAnchor.CENTER; 221 } else if (refPt.equals(RefPt2D.CENTER_RIGHT)) { 222 return vflip ? TextAnchor.CENTER_RIGHT : TextAnchor.CENTER_LEFT; 223 } else if (refPt.equals(RefPt2D.BOTTOM_LEFT)) { 224 return vflip ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_RIGHT; 225 } else if (refPt.equals(RefPt2D.BOTTOM_CENTER)) { 226 return vflip ? TextAnchor.BOTTOM_CENTER : TextAnchor.TOP_CENTER; 227 } else if (refPt.equals(RefPt2D.BOTTOM_RIGHT)) { 228 return vflip ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_LEFT; 229 } 230 throw new RuntimeException("Unknown refPt " + refPt); 231 } 232 233 /** 234 * A utility method that returns a suitable text anchor for a given 235 * reference point relative to a line (rather than a rectangle which is 236 * the normal case). This is used for value marker label positioning. 237 * 238 * @param refPt the reference point ({@code null} not permitted). 239 * @param vflip is the text flipped vertically? 240 * 241 * @return A text anchor (never {@code null}). 242 */ 243 protected static TextAnchor deriveTextAnchorForLine(RefPt2D refPt, 244 boolean vflip) { 245 if (refPt.equals(RefPt2D.TOP_LEFT)) { 246 return vflip ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_RIGHT; 247 } else if (refPt.equals(RefPt2D.TOP_CENTER)) { 248 return vflip ? TextAnchor.BOTTOM_CENTER : TextAnchor.TOP_CENTER; 249 } else if (refPt.equals(RefPt2D.TOP_RIGHT)) { 250 return vflip ? TextAnchor.BOTTOM_RIGHT :TextAnchor.TOP_LEFT; 251 } if (refPt.equals(RefPt2D.CENTER_LEFT)) { 252 return vflip ? TextAnchor.CENTER_LEFT : TextAnchor.CENTER_RIGHT; 253 } else if (refPt.equals(RefPt2D.CENTER)) { 254 return TextAnchor.CENTER; 255 } else if (refPt.equals(RefPt2D.CENTER_RIGHT)) { 256 return vflip ? TextAnchor.CENTER_RIGHT : TextAnchor.CENTER_LEFT; 257 } else if (refPt.equals(RefPt2D.BOTTOM_LEFT)) { 258 return vflip ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_RIGHT; 259 } else if (refPt.equals(RefPt2D.BOTTOM_CENTER)) { 260 return vflip ? TextAnchor.TOP_CENTER : TextAnchor.BOTTOM_CENTER; 261 } else if (refPt.equals(RefPt2D.BOTTOM_RIGHT)) { 262 return vflip ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_LEFT; 263 } 264 throw new RuntimeException("Unknown refPt " + refPt); 265 } 266 267}