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