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.axis; 034 035import java.awt.BasicStroke; 036import java.awt.Color; 037import java.awt.Font; 038import java.awt.Graphics2D; 039import java.awt.Shape; 040import java.awt.Stroke; 041import java.awt.geom.Line2D; 042import java.awt.geom.Point2D; 043import java.io.IOException; 044import java.io.ObjectInputStream; 045import java.io.ObjectOutputStream; 046import java.io.Serializable; 047import java.util.HashMap; 048import java.util.List; 049import java.util.Map; 050 051import javax.swing.event.EventListenerList; 052import org.jfree.chart3d.internal.Args; 053import org.jfree.chart3d.internal.ObjectUtils; 054import org.jfree.chart3d.internal.SerialUtils; 055import org.jfree.chart3d.internal.TextUtils; 056import org.jfree.chart3d.Chart3DHints; 057import org.jfree.chart3d.ChartElementVisitor; 058import org.jfree.chart3d.graphics2d.TextAnchor; 059import org.jfree.chart3d.graphics3d.RenderedElement; 060import org.jfree.chart3d.graphics3d.RenderingInfo; 061import org.jfree.chart3d.graphics3d.internal.Utils2D; 062import org.jfree.chart3d.interaction.InteractiveElementType; 063import org.jfree.chart3d.marker.MarkerChangeEvent; 064import org.jfree.chart3d.marker.MarkerChangeListener; 065import org.jfree.chart3d.plot.CategoryPlot3D; 066 067/** 068 * A base class that can be used to create an {@link Axis3D} implementation. 069 * This class implements the core axis attributes as well as the change 070 * listener mechanism required to enable automatic repainting of charts. 071 * <br><br> 072 * NOTE: This class is serializable, but the serialization format is subject 073 * to change in future releases and should not be relied upon for persisting 074 * instances of this class. 075 */ 076public abstract class AbstractAxis3D implements Axis3D, MarkerChangeListener, 077 Serializable { 078 079 /** 080 * The default axis label font (in most circumstances this will be 081 * overridden by the chart style). 082 * 083 * @since 1.2 084 */ 085 public static final Font DEFAULT_LABEL_FONT = new Font("Dialog", Font.BOLD, 086 12); 087 088 /** 089 * The default axis label color (in most circumstances this will be 090 * overridden by the chart style). 091 * 092 * @since 1.2 093 */ 094 public static final Color DEFAULT_LABEL_COLOR = Color.BLACK; 095 096 /** 097 * The default label offset. 098 * 099 * @since 1.2 100 */ 101 public static final double DEFAULT_LABEL_OFFSET = 10; 102 103 /** 104 * The default tick label font (in most circumstances this will be 105 * overridden by the chart style). 106 * 107 * @since 1.2 108 */ 109 public static final Font DEFAULT_TICK_LABEL_FONT = new Font("Dialog", 110 Font.PLAIN, 12); 111 112 /** 113 * The default tick label color (in most circumstances this will be 114 * overridden by the chart style). 115 * 116 * @since 1.2 117 */ 118 public static final Color DEFAULT_TICK_LABEL_COLOR = Color.BLACK; 119 120 /** 121 * The default stroke for the axis line. 122 * 123 * @since 1.2 124 */ 125 public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(0f); 126 127 /** 128 * The default color for the axis line. 129 * 130 * @since 1.2 131 */ 132 public static final Color DEFAULT_LINE_COLOR = Color.GRAY; 133 134 /** A flag that determines whether or not the axis will be drawn. */ 135 private boolean visible; 136 137 /** The axis label (if {@code null}, no label is displayed). */ 138 private String label; 139 140 /** The label font (never {@code null}). */ 141 private Font labelFont; 142 143 /** The color used to draw the axis label (never {@code null}). */ 144 private Color labelColor; 145 146 /** The offset between the tick labels and the label. */ 147 private double labelOffset; 148 149 /** The stroke used to draw the axis line. */ 150 private transient Stroke lineStroke; 151 152 /** The color used to draw the axis line. */ 153 private Color lineColor; 154 155 /** Draw the tick labels? */ 156 private boolean tickLabelsVisible; 157 158 /** The font used to display tick labels (never {@code null}) */ 159 private Font tickLabelFont; 160 161 /** The tick label paint (never {@code null}). */ 162 private Color tickLabelColor; 163 164 /** Storage for registered change listeners. */ 165 private final transient EventListenerList listenerList; 166 167 /** 168 * Creates a new label with the specified label. If the supplied label 169 * is {@code null}, the axis will be shown without a label. 170 * 171 * @param label the axis label ({@code null} permitted). 172 */ 173 public AbstractAxis3D(String label) { 174 this.visible = true; 175 this.label = label; 176 this.labelFont = DEFAULT_LABEL_FONT; 177 this.labelColor = DEFAULT_LABEL_COLOR; 178 this.labelOffset = DEFAULT_LABEL_OFFSET; 179 this.lineStroke = DEFAULT_LINE_STROKE; 180 this.lineColor = DEFAULT_LINE_COLOR; 181 this.tickLabelsVisible = true; 182 this.tickLabelFont = DEFAULT_TICK_LABEL_FONT; 183 this.tickLabelColor = DEFAULT_TICK_LABEL_COLOR; 184 this.listenerList = new EventListenerList(); 185 } 186 187 /** 188 * Returns the flag that determines whether or not the axis is drawn 189 * on the chart. 190 * 191 * @return A boolean. 192 * 193 * @see #setVisible(boolean) 194 */ 195 @Override 196 public boolean isVisible() { 197 return this.visible; 198 } 199 200 /** 201 * Sets the flag that determines whether or not the axis is drawn on the 202 * chart and sends an {@link Axis3DChangeEvent} to all registered listeners. 203 * 204 * @param visible the flag. 205 * 206 * @see #isVisible() 207 */ 208 @Override 209 public void setVisible(boolean visible) { 210 this.visible = visible; 211 fireChangeEvent(false); 212 } 213 214 /** 215 * Returns the axis label - the text that describes what the axis measures. 216 * The description should usually specify the units. When this attribute 217 * is {@code null}, the axis is drawn without a label. 218 * 219 * @return The axis label (possibly {@code null}). 220 */ 221 public String getLabel() { 222 return this.label; 223 } 224 225 /** 226 * Sets the axis label and sends an {@link Axis3DChangeEvent} to all 227 * registered listeners. If the supplied label is {@code null}, 228 * the axis will be drawn without a label. 229 * 230 * @param label the label ({@code null} permitted). 231 */ 232 public void setLabel(String label) { 233 this.label = label; 234 fireChangeEvent(false); 235 } 236 237 /** 238 * Returns the font used to display the main axis label. The default value 239 * is {@code Font("SansSerif", Font.BOLD, 12)}. 240 * 241 * @return The font used to display the axis label (never {@code null}). 242 */ 243 @Override 244 public Font getLabelFont() { 245 return this.labelFont; 246 } 247 248 /** 249 * Sets the font used to display the main axis label and sends an 250 * {@link Axis3DChangeEvent} to all registered listeners. 251 * 252 * @param font the new font ({@code null} not permitted). 253 */ 254 @Override 255 public void setLabelFont(Font font) { 256 Args.nullNotPermitted(font, "font"); 257 this.labelFont = font; 258 fireChangeEvent(false); 259 } 260 261 /** 262 * Returns the color used for the label. The default value is 263 * {@code Color.BLACK}. 264 * 265 * @return The label paint (never {@code null}). 266 */ 267 @Override 268 public Color getLabelColor() { 269 return this.labelColor; 270 } 271 272 /** 273 * Sets the color used to draw the axis label and sends an 274 * {@link Axis3DChangeEvent} to all registered listeners. 275 * 276 * @param color the color ({@code null} not permitted). 277 */ 278 @Override 279 public void setLabelColor(Color color) { 280 Args.nullNotPermitted(color, "color"); 281 this.labelColor = color; 282 fireChangeEvent(false); 283 } 284 285 /** 286 * Returns the offset between the tick labels and the axis label, measured 287 * in Java2D units. The default value is {@link #DEFAULT_LABEL_OFFSET}. 288 * 289 * @return The offset. 290 * 291 * @since 1.2 292 */ 293 public double getLabelOffset() { 294 return this.labelOffset; 295 } 296 297 /** 298 * Sets the offset between the tick labels and the axis label and sends 299 * an {@link Axis3DChangeEvent} to all registered listeners. 300 * 301 * @param offset the offset. 302 * 303 * @since 1.2 304 */ 305 public void setLabelOffset(double offset) { 306 this.labelOffset = offset; 307 fireChangeEvent(false); 308 } 309 310 /** 311 * Returns the stroke used to draw the axis line. The default value is 312 * {@link #DEFAULT_LINE_STROKE}. 313 * 314 * @return The stroke used to draw the axis line (never {@code null}). 315 */ 316 public Stroke getLineStroke() { 317 return this.lineStroke; 318 } 319 320 /** 321 * Sets the stroke used to draw the axis line and sends an 322 * {@link Axis3DChangeEvent} to all registered listeners. 323 * 324 * @param stroke the new stroke ({@code null} not permitted). 325 */ 326 public void setLineStroke(Stroke stroke) { 327 Args.nullNotPermitted(stroke, "stroke"); 328 this.lineStroke = stroke; 329 fireChangeEvent(false); 330 } 331 332 /** 333 * Returns the color used to draw the axis line. The default value is 334 * {@link #DEFAULT_LINE_COLOR}. 335 * 336 * @return The color used to draw the axis line (never {@code null}). 337 */ 338 public Color getLineColor() { 339 return this.lineColor; 340 } 341 342 /** 343 * Sets the color used to draw the axis line and sends an 344 * {@link Axis3DChangeEvent} to all registered listeners. 345 * 346 * @param color the new color ({@code null} not permitted). 347 */ 348 public void setLineColor(Color color) { 349 Args.nullNotPermitted(color, "color"); 350 this.lineColor = color; 351 fireChangeEvent(false); 352 } 353 354 /** 355 * Returns the flag that controls whether or not the tick labels are 356 * drawn. The default value is {@code true}. 357 * 358 * @return A boolean. 359 */ 360 public boolean getTickLabelsVisible() { 361 return this.tickLabelsVisible; 362 } 363 364 /** 365 * Sets the flag that controls whether or not the tick labels are drawn, 366 * and sends a change event to all registered listeners. You should think 367 * carefully before setting this flag to {@code false}, because if 368 * the tick labels are not shown it will be hard for the reader to 369 * understand the resulting chart. 370 * 371 * @param visible visible? 372 */ 373 public void setTickLabelsVisible(boolean visible) { 374 this.tickLabelsVisible = visible; 375 fireChangeEvent(false); 376 } 377 378 /** 379 * Returns the font used to display the tick labels. The default value 380 * is {@link #DEFAULT_TICK_LABEL_FONT}. 381 * 382 * @return The font (never {@code null}). 383 */ 384 @Override 385 public Font getTickLabelFont() { 386 return this.tickLabelFont; 387 } 388 389 /** 390 * Sets the font used to display tick labels and sends an 391 * {@link Axis3DChangeEvent} to all registered listeners. 392 * 393 * @param font the font ({@code null} not permitted). 394 */ 395 @Override 396 public void setTickLabelFont(Font font) { 397 Args.nullNotPermitted(font, "font"); 398 this.tickLabelFont = font; 399 fireChangeEvent(false); 400 } 401 402 /** 403 * Returns the foreground color for the tick labels. The default value 404 * is {@link #DEFAULT_LABEL_COLOR}. 405 * 406 * @return The foreground color (never {@code null}). 407 */ 408 @Override 409 public Color getTickLabelColor() { 410 return this.tickLabelColor; 411 } 412 413 /** 414 * Sets the foreground color for the tick labels and sends an 415 * {@link Axis3DChangeEvent} to all registered listeners. 416 * 417 * @param color the color ({@code null} not permitted). 418 */ 419 @Override 420 public void setTickLabelColor(Color color) { 421 Args.nullNotPermitted(color, "color"); 422 this.tickLabelColor = color; 423 fireChangeEvent(false); 424 } 425 426 /** 427 * Receives a {@link ChartElementVisitor}. This method is part of a general 428 * mechanism for traversing the chart structure and performing operations 429 * on each element in the chart. You will not normally call this method 430 * directly. 431 * 432 * @param visitor the visitor ({@code null} not permitted). 433 * 434 * @since 1.2 435 */ 436 @Override 437 public abstract void receive(ChartElementVisitor visitor); 438 439 /** 440 * Draws the specified text as the axis label and returns a bounding 441 * shape (2D) for the text. 442 * 443 * @param label the label ({@code null} not permitted). 444 * @param g2 the graphics target ({@code null} not permitted). 445 * @param axisLine the axis line ({@code null} not permitted). 446 * @param opposingPt an opposing point ({@code null} not permitted). 447 * @param offset the offset. 448 * @param info collects rendering info ({@code null} permitted). 449 * @param hinting perform element hinting? 450 * 451 * @return A bounding shape. 452 */ 453 protected Shape drawAxisLabel(String label, Graphics2D g2, 454 Line2D axisLine, Point2D opposingPt, double offset, 455 RenderingInfo info, boolean hinting) { 456 Args.nullNotPermitted(label, "label"); 457 Args.nullNotPermitted(g2, "g2"); 458 Args.nullNotPermitted(axisLine, "axisLine"); 459 Args.nullNotPermitted(opposingPt, "opposingPt"); 460 g2.setFont(getLabelFont()); 461 g2.setPaint(getLabelColor()); 462 Line2D labelPosLine = Utils2D.createPerpendicularLine(axisLine, 0.5, 463 offset, opposingPt); 464 double theta = Utils2D.calculateTheta(axisLine); 465 if (theta < -Math.PI / 2.0) { 466 theta = theta + Math.PI; 467 } 468 if (theta > Math.PI / 2.0) { 469 theta = theta - Math.PI; 470 } 471 472 if (hinting) { 473 Map<String, String> m = new HashMap<>(); 474 m.put("ref", "{\"type\": \"axisLabel\", \"axis\": \"" + axisStr() 475 + "\", \"label\": \"" + getLabel() + "\"}"); 476 g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); 477 } 478 Shape bounds = TextUtils.drawRotatedString(getLabel(), g2, 479 (float) labelPosLine.getX2(), (float) labelPosLine.getY2(), 480 TextAnchor.CENTER, theta, TextAnchor.CENTER); 481 if (hinting) { 482 g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true); 483 } 484 if (info != null) { 485 RenderedElement labelElement = new RenderedElement( 486 InteractiveElementType.AXIS_LABEL, bounds); 487 labelElement.setProperty("axis", axisStr()); 488 labelElement.setProperty("label", getLabel()); 489 info.addOffsetElement(labelElement); 490 } 491 return bounds; 492 } 493 494 /** 495 * Returns a string representing the configured type of the axis ("row", 496 * "column", "value", "x", "y" or "z" - other values may be possible in the 497 * future). A <em>row</em> axis on a {@link CategoryPlot3D} is in the 498 * position of a z-axis (depth), a <em>column</em> axis is in the position 499 * of an x-axis (width), a <em>value</em> axis is in the position of a 500 * y-axis (height). 501 * 502 * @return A string (never {@code null}). 503 * 504 * @since 1.3 505 */ 506 protected abstract String axisStr(); 507 508 /** 509 * Draws the axis along an arbitrary line (between {@code startPt} 510 * and {@code endPt}). The opposing point is used as a reference 511 * point to know on which side of the axis to draw the labels. 512 * 513 * @param g2 the graphics target ({@code null} not permitted). 514 * @param startPt the starting point ({@code null} not permitted). 515 * @param endPt the end point ({@code null} not permitted) 516 * @param opposingPt an opposing point ({@code null} not permitted). 517 * @param tickData info about the ticks to draw ({@code null} not 518 * permitted). 519 * @param info an object to be populated with rendering info 520 * ({@code null} permitted). 521 * @param hinting a flag that controls whether or not element hinting 522 * should be performed. 523 */ 524 @Override 525 public abstract void draw(Graphics2D g2, Point2D startPt, Point2D endPt, 526 Point2D opposingPt, List<TickData> tickData, RenderingInfo info, 527 boolean hinting); 528 529 /** 530 * Tests this instance for equality with an arbitrary object. 531 * 532 * @param obj the object to test against ({@code null} permitted). 533 * 534 * @return A boolean. 535 */ 536 @Override 537 public boolean equals(Object obj) { 538 if (obj == this) { 539 return true; 540 } 541 if (!(obj instanceof AbstractAxis3D)) { 542 return false; 543 } 544 AbstractAxis3D that = (AbstractAxis3D) obj; 545 if (this.visible != that.visible) { 546 return false; 547 } 548 if (!ObjectUtils.equals(this.label, that.label)) { 549 return false; 550 } 551 if (!this.labelFont.equals(that.labelFont)) { 552 return false; 553 } 554 if (!this.labelColor.equals(that.labelColor)) { 555 return false; 556 } 557 if (!this.lineStroke.equals(that.lineStroke)) { 558 return false; 559 } 560 if (!this.lineColor.equals(that.lineColor)) { 561 return false; 562 } 563 if (this.tickLabelsVisible != that.tickLabelsVisible) { 564 return false; 565 } 566 if (!this.tickLabelFont.equals(that.tickLabelFont)) { 567 return false; 568 } 569 if (!this.tickLabelColor.equals(that.tickLabelColor)) { 570 return false; 571 } 572 return true; 573 } 574 575 /** 576 * Returns a hash code for this instance. 577 * 578 * @return A hash code. 579 */ 580 @Override 581 public int hashCode() { 582 int hash = 5; 583 hash = 83 * hash + (this.visible ? 1 : 0); 584 hash = 83 * hash + ObjectUtils.hashCode(this.label); 585 hash = 83 * hash + ObjectUtils.hashCode(this.labelFont); 586 hash = 83 * hash + ObjectUtils.hashCode(this.labelColor); 587 hash = 83 * hash + ObjectUtils.hashCode(this.lineStroke); 588 hash = 83 * hash + ObjectUtils.hashCode(this.lineColor); 589 hash = 83 * hash + (this.tickLabelsVisible ? 1 : 0); 590 hash = 83 * hash + ObjectUtils.hashCode(this.tickLabelFont); 591 hash = 83 * hash + ObjectUtils.hashCode(this.tickLabelColor); 592 return hash; 593 } 594 595 /** 596 * Registers a listener so that it will receive axis change events. 597 * 598 * @param listener the listener ({@code null} not permitted). 599 */ 600 @Override 601 public void addChangeListener(Axis3DChangeListener listener) { 602 this.listenerList.add(Axis3DChangeListener.class, listener); 603 } 604 605 /** 606 * Unregisters a listener so that it will no longer receive axis 607 * change events. 608 * 609 * @param listener the listener ({@code null} not permitted). 610 */ 611 @Override 612 public void removeChangeListener(Axis3DChangeListener listener) { 613 this.listenerList.remove(Axis3DChangeListener.class, listener); 614 } 615 616 /** 617 * Notifies all registered listeners that the axis has been modified. 618 * 619 * @param event information about the change event. 620 */ 621 public void notifyListeners(Axis3DChangeEvent event) { 622 Object[] listeners = this.listenerList.getListenerList(); 623 for (int i = listeners.length - 2; i >= 0; i -= 2) { 624 if (listeners[i] == Axis3DChangeListener.class) { 625 ((Axis3DChangeListener) listeners[i + 1]).axisChanged(event); 626 } 627 } 628 } 629 630 /** 631 * Sends an {@link Axis3DChangeEvent} to all registered listeners. 632 * 633 * @param requiresWorldUpdate a flag indicating whether this change 634 * requires the 3D world to be updated. 635 */ 636 protected void fireChangeEvent(boolean requiresWorldUpdate) { 637 notifyListeners(new Axis3DChangeEvent(this, requiresWorldUpdate)); 638 } 639 640 /** 641 * Receives notification of a change to a marker managed by this axis - the 642 * response is to fire a change event for the axis (to eventually trigger 643 * a repaint of the chart). Marker changes don't require the world model 644 * to be updated. 645 * 646 * @param event the event. 647 * 648 * @since 1.2 649 */ 650 @Override 651 public void markerChanged(MarkerChangeEvent event) { 652 fireChangeEvent(false); 653 } 654 655 /** 656 * Provides serialization support. 657 * 658 * @param stream the output stream. 659 * 660 * @throws IOException if there is an I/O error. 661 */ 662 private void writeObject(ObjectOutputStream stream) throws IOException { 663 stream.defaultWriteObject(); 664 SerialUtils.writeStroke(this.lineStroke, stream); 665 } 666 667 /** 668 * Provides serialization support. 669 * 670 * @param stream the input stream. 671 * 672 * @throws IOException if there is an I/O error. 673 * @throws ClassNotFoundException if there is a classpath problem. 674 */ 675 private void readObject(ObjectInputStream stream) 676 throws IOException, ClassNotFoundException { 677 stream.defaultReadObject(); 678 this.lineStroke = SerialUtils.readStroke(stream); 679 } 680 681}