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; 034 035import java.awt.BasicStroke; 036import java.awt.Stroke; 037import java.awt.geom.Line2D; 038import java.awt.Color; 039import java.awt.Graphics2D; 040import java.awt.Font; 041import java.awt.Paint; 042import java.awt.RenderingHints; 043import java.awt.Shape; 044import java.awt.geom.AffineTransform; 045import java.awt.geom.Path2D; 046import java.awt.geom.Point2D; 047import java.awt.geom.Dimension2D; 048import java.awt.geom.Rectangle2D; 049import java.io.Serializable; 050import java.io.IOException; 051import java.io.ObjectInputStream; 052import java.util.ArrayList; 053import java.util.Collections; 054import java.util.List; 055import java.util.HashMap; 056import java.util.Map; 057 058import javax.swing.event.EventListenerList; 059 060import com.orsoncharts.ChartBox3D.ChartBoxFace; 061import com.orsoncharts.axis.Axis3D; 062import com.orsoncharts.axis.TickData; 063import com.orsoncharts.axis.ValueAxis3D; 064import com.orsoncharts.data.ItemKey; 065import com.orsoncharts.graphics3d.Dimension3D; 066import com.orsoncharts.graphics3d.DoubleSidedFace; 067import com.orsoncharts.graphics3d.Drawable3D; 068import com.orsoncharts.graphics3d.Face; 069import com.orsoncharts.graphics3d.FaceSorter; 070import com.orsoncharts.graphics3d.LabelFace; 071import com.orsoncharts.graphics3d.Object3D; 072import com.orsoncharts.graphics3d.Point3D; 073import com.orsoncharts.graphics3d.Utils2D; 074import com.orsoncharts.graphics3d.ViewPoint3D; 075import com.orsoncharts.graphics3d.World; 076import com.orsoncharts.legend.LegendAnchor; 077import com.orsoncharts.plot.CategoryPlot3D; 078import com.orsoncharts.plot.PiePlot3D; 079import com.orsoncharts.plot.Plot3DChangeEvent; 080import com.orsoncharts.plot.Plot3DChangeListener; 081import com.orsoncharts.plot.Plot3D; 082import com.orsoncharts.plot.XYZPlot; 083import com.orsoncharts.graphics3d.Offset2D; 084import com.orsoncharts.graphics3d.RenderingInfo; 085import com.orsoncharts.graphics3d.StandardFaceSorter; 086import com.orsoncharts.graphics3d.RenderedElement; 087import com.orsoncharts.interaction.InteractiveElementType; 088import com.orsoncharts.legend.LegendBuilder; 089import com.orsoncharts.legend.StandardLegendBuilder; 090import com.orsoncharts.marker.Marker; 091import com.orsoncharts.marker.MarkerData; 092import com.orsoncharts.style.ChartStyle; 093import com.orsoncharts.style.ChartStyleChangeEvent; 094import com.orsoncharts.style.ChartStyleChangeListener; 095import com.orsoncharts.style.ChartStyler; 096import com.orsoncharts.table.GradientRectanglePainter; 097import com.orsoncharts.table.GridElement; 098import com.orsoncharts.table.HAlign; 099import com.orsoncharts.table.TableElement; 100import com.orsoncharts.table.TextElement; 101import com.orsoncharts.table.StandardRectanglePainter; 102import com.orsoncharts.table.RectanglePainter; 103import com.orsoncharts.util.Anchor2D; 104import com.orsoncharts.util.ArgChecks; 105import com.orsoncharts.util.ObjectUtils; 106import com.orsoncharts.util.Orientation; 107import com.orsoncharts.util.RefPt2D; 108import com.orsoncharts.util.TextUtils; 109import com.orsoncharts.util.TextAnchor; 110 111/** 112 * A chart object for 3D charts (this is the umbrella object that manages all 113 * the components of the chart). The {@link Chart3DFactory} class provides 114 * some factory methods to construct common types of charts. 115 * <br><br> 116 * All rendering is done via the Java2D API, so this object is able to draw to 117 * any implementation of the Graphics2D API (including 118 * <a href="http://www.jfree.org/jfreesvg/" target="JFreeSVG">JFreeSVG</a> for 119 * SVG output, and 120 * <a href="http://www.object-refinery.com/pdf/" target="OrsonPDF">OrsonPDF</a> 121 * for PDF output). 122 * <br><br> 123 * In the step prior to rendering, a chart is composed in a 3D model that is 124 * referred to as the "world". The dimensions of this 3D model are measured 125 * in "world units" and the overall size is controlled by the plot. You will 126 * see some attributes in the API that are specified in "world units", and these 127 * can be used to modify how objects are composed within the 3D world model. 128 * Once the objects (for example, bars in a bar chart) within the world have 129 * been composed, they are projected onto a 2D plane and rendered to the 130 * {@code Graphics2D} target (such as the screen, image, SVG file or 131 * PDF file). 132 * <br><br> 133 * Charts can have simple titles or composite titles (anything that can be 134 * constructed as a {@link TableElement} instance. The {@link TitleUtils} 135 * class contains methods to create a common title/subtitle composite title. 136 * This is illustrated in some of the demo applications. The chart title 137 * and legend (and also the axis labels) are not part of the 3D world model, 138 * they are overlaid on the output after the 3D components have been 139 * rendered. 140 * <br><br> 141 * NOTE: This class is serializable, but the serialization format is subject 142 * to change in future releases and should not be relied upon for persisting 143 * instances of this class. 144 * 145 * @see Chart3DFactory 146 * @see Chart3DPanel 147 */ 148@SuppressWarnings("serial") 149public class Chart3D implements Drawable3D, ChartElement, 150 Plot3DChangeListener, ChartStyleChangeListener, Serializable { 151 152 /** 153 * The default projection distance. 154 * 155 * @since 1.2 156 */ 157 public static final double DEFAULT_PROJ_DIST = 1500.0; 158 159 /** 160 * The key for a property that stores the interactive element type. 161 * 162 * @since 1.3 163 */ 164 public static final String INTERACTIVE_ELEMENT_TYPE 165 = "interactive_element_type"; 166 167 /** 168 * The key for a property that stores the series key. This is used to 169 * store the series key on the {@link TableElement} representing a legend 170 * item, and also on a corresponding {@link RenderedElement} after 171 * chart rendering (in the {@link RenderingInfo}). 172 * 173 * @since 1.3 174 */ 175 public static final String SERIES_KEY = "series_key"; 176 177 /** The chart id. */ 178 private String id; 179 180 /** A background rectangle painter, if any. */ 181 private RectanglePainter background; 182 183 /** The chart title (can be {@code null}). */ 184 private TableElement title; 185 186 /** The anchor point for the title (never {@code null}). */ 187 private Anchor2D titleAnchor; 188 189 /** A builder for the chart legend (can be {@code null}). */ 190 private LegendBuilder legendBuilder; 191 192 /** The anchor point for the legend (never {@code null}). */ 193 private Anchor2D legendAnchor; 194 195 /** The orientation for the legend (never {@code null}). */ 196 private Orientation legendOrientation; 197 198 /** The plot. */ 199 private Plot3D plot; 200 201 /** The view point. */ 202 private ViewPoint3D viewPoint; 203 204 /** The projection distance. */ 205 private double projDist; 206 207 /** The chart box color (never {@code null}). */ 208 private Color chartBoxColor; 209 210 /** 211 * A translation factor applied to the chart when drawing. We use this 212 * to allow the user (optionally) to drag the chart from its center 213 * location to better align it with the chart title and legend. 214 */ 215 private Offset2D translate2D; 216 217 /** Storage for registered change listeners. */ 218 private transient EventListenerList listenerList; 219 220 /** 221 * A flag that controls whether or not the chart will notify listeners 222 * of changes (defaults to {@code true}, but sometimes it is useful 223 * to disable this). 224 */ 225 private boolean notify; 226 227 /** 228 * Rendering hints that will be used for chart drawing. This can be 229 * empty but it should never be {@code null}. 230 * 231 * @since 1.1 232 */ 233 private transient RenderingHints renderingHints; 234 235 /** 236 * The chart style. 237 * 238 * @since 1.2 239 */ 240 private ChartStyle style; 241 242 /** A 3D model of the world (represents the chart). */ 243 private transient World world; 244 245 /** An object that sorts faces for rendering (painter's algorithm). */ 246 private FaceSorter faceSorter; 247 248 /** 249 * A flag that controls whether or not element hints are added to the 250 * {@code Graphics2D} output. 251 */ 252 private boolean elementHinting; 253 254 /** 255 * Creates a 3D chart for the specified plot using the default chart 256 * style. Note that a plot instance must be used in one chart instance 257 * only. 258 * 259 * @param title the chart title ({@code null} permitted). 260 * @param subtitle the chart subtitle ({@code null} permitted). 261 * @param plot the plot ({@code null} not permitted). 262 * 263 * @see Chart3DFactory 264 */ 265 public Chart3D(String title, String subtitle, Plot3D plot) { 266 this(title, subtitle, plot, Chart3DFactory.getDefaultChartStyle()); 267 } 268 269 /** 270 * Creates a 3D chart for the specified plot using the supplied style. 271 * 272 * @param title the chart title ({@code null} permitted). 273 * @param subtitle the chart subtitle ({@code null} permitted). 274 * @param plot the plot ({@code null} not permitted). 275 * @param style the chart style ({@code null} not permitted). 276 * 277 * @since 1.2 278 */ 279 public Chart3D(String title, String subtitle, Plot3D plot, 280 ChartStyle style) { 281 ArgChecks.nullNotPermitted(plot, "plot"); 282 ArgChecks.nullNotPermitted(style, "style"); 283 plot.setChart(this); 284 this.background = new StandardRectanglePainter(Color.WHITE); 285 if (title != null) { 286 this.title = TitleUtils.createTitle(title, subtitle); 287 } 288 this.titleAnchor = TitleAnchor.TOP_LEFT; 289 this.legendBuilder = new StandardLegendBuilder(); 290 this.legendAnchor = LegendAnchor.BOTTOM_RIGHT; 291 this.legendOrientation = Orientation.HORIZONTAL; 292 this.plot = plot; 293 this.plot.addChangeListener(this); 294 Dimension3D dim = this.plot.getDimensions(); 295 float distance = (float) dim.getDiagonalLength() * 3.0f; 296 this.viewPoint = ViewPoint3D.createAboveViewPoint(distance); 297 this.projDist = DEFAULT_PROJ_DIST; 298 this.chartBoxColor = new Color(255, 255, 255, 100); 299 this.translate2D = new Offset2D(); 300 this.faceSorter = new StandardFaceSorter(); 301 this.renderingHints = new RenderingHints( 302 RenderingHints.KEY_ANTIALIASING, 303 RenderingHints.VALUE_ANTIALIAS_ON); 304 this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, 305 RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 306 this.elementHinting = false; 307 this.notify = true; 308 this.listenerList = new EventListenerList(); 309 this.style = style; 310 this.style.addChangeListener(this); 311 receive(new ChartStyler(this.style)); 312 } 313 314 /** 315 * Returns the chart id. 316 * 317 * @return The chart id (possibly {@code null}). 318 * 319 * @since 1.3 320 */ 321 public String getID() { 322 return this.id; 323 } 324 325 /** 326 * Sets the chart id. 327 * 328 * @param id the id ({@code null} permitted). 329 * 330 * @since 1.3 331 */ 332 public void setID(String id) { 333 this.id = id; 334 } 335 336 /** 337 * Returns the background painter (an object that is responsible for filling 338 * the background area before charts are rendered). The default value 339 * is an instance of {@link StandardRectanglePainter} that paints the 340 * background white. 341 * 342 * @return The background painter (possibly {@code null}). 343 * 344 * @see #setBackground(com.orsoncharts.table.RectanglePainter) 345 */ 346 public RectanglePainter getBackground() { 347 return this.background; 348 } 349 350 /** 351 * Sets the background painter and sends a {@link Chart3DChangeEvent} to 352 * all registered listeners. A background painter is used to fill in the 353 * background of the chart before the 3D rendering takes place. To fill 354 * the background with a color or image, you can use 355 * {@link StandardRectanglePainter}. To fill the background with a 356 * gradient paint, use {@link GradientRectanglePainter}. 357 * 358 * @param background the background painter ({@code null} permitted). 359 * 360 * @see #getBackground() 361 */ 362 public void setBackground(RectanglePainter background) { 363 this.background = background; 364 fireChangeEvent(); 365 } 366 367 /** 368 * Returns the chart title. A {@link TableElement} is used for the title, 369 * since it allows a lot of flexibility in the types of title that can 370 * be displayed. 371 * 372 * @return The chart title (possibly {@code null}). 373 */ 374 public TableElement getTitle() { 375 return this.title; 376 } 377 378 /** 379 * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 380 * registered listeners. This is a convenience method that constructs 381 * the required {@link TableElement} under-the-hood. 382 * 383 * @param title the title ({@code null} permitted). 384 */ 385 public void setTitle(String title) { 386 if (title == null) { 387 setTitle((TableElement) null); 388 } else { 389 setTitle(title, this.style.getTitleFont(), 390 TitleUtils.DEFAULT_TITLE_COLOR); 391 } 392 } 393 394 /** 395 * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 396 * registered listeners. This is a convenience method that constructs 397 * the required {@link TableElement} under-the-hood. 398 * 399 * @param title the title ({@code null} not permitted). 400 * @param font the font ({@code null} not permitted). 401 * @param color the foreground color ({@code null} not permitted). 402 */ 403 public void setTitle(String title, Font font, Color color) { 404 // defer 'title' null check 405 ArgChecks.nullNotPermitted(font, "font"); 406 ArgChecks.nullNotPermitted(color, "color"); 407 TextElement te = new TextElement(title); 408 te.setTag("CHART_TITLE"); 409 te.setFont(font); 410 te.setColor(color); 411 setTitle(te); 412 } 413 414 /** 415 * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 416 * registered listeners. You can set the title to {@code null}, in 417 * which case there will be no chart title. 418 * 419 * @param title the title ({@code null} permitted). 420 */ 421 public void setTitle(TableElement title) { 422 this.title = title; 423 fireChangeEvent(); 424 } 425 426 /** 427 * Returns the title anchor. This controls the position of the title 428 * in the chart area. 429 * 430 * @return The title anchor (never {@code null}). 431 * 432 * @see #setTitleAnchor(com.orsoncharts.util.Anchor2D) 433 */ 434 public Anchor2D getTitleAnchor() { 435 return this.titleAnchor; 436 } 437 438 /** 439 * Sets the title anchor and sends a {@link Chart3DChangeEvent} to all 440 * registered listeners. There is a {@link TitleAnchor} class providing 441 * some useful default anchors. 442 * 443 * @param anchor the anchor ({@code null} not permitted). 444 * 445 * @see #getTitleAnchor() 446 */ 447 public void setTitleAnchor(Anchor2D anchor) { 448 ArgChecks.nullNotPermitted(anchor, "anchor"); 449 this.titleAnchor = anchor; 450 fireChangeEvent(); 451 } 452 453 /** 454 * Returns the plot, which manages the dataset, the axes (if any), the 455 * renderer (if any) and other attributes related to plotting data. The 456 * plot is specified via the constructor...there is no method to set a 457 * new plot for the chart, instead you need to create a new chart instance. 458 * 459 * @return The plot (never {@code null}). 460 */ 461 public Plot3D getPlot() { 462 return this.plot; 463 } 464 465 /** 466 * Returns the chart box color (the chart box is the visible, open-sided 467 * box inside which data is plotted for all charts except pie charts). 468 * The default value is {@code Color.WHITE}. 469 * 470 * @return The chart box color (never {@code null}). 471 * 472 * @see #setChartBoxColor(java.awt.Color) 473 */ 474 public Color getChartBoxColor() { 475 return this.chartBoxColor; 476 } 477 478 /** 479 * Sets the chart box color and sends a {@link Chart3DChangeEvent} to all 480 * registered listeners. Bear in mind that {@link PiePlot3D} does not 481 * display a chart box, so this attribute will be ignored for pie charts. 482 * 483 * @param color the color ({@code null} not permitted). 484 * 485 * @see #getChartBoxColor() 486 */ 487 public void setChartBoxColor(Color color) { 488 ArgChecks.nullNotPermitted(color, "color"); 489 this.chartBoxColor = color; 490 fireChangeEvent(); 491 } 492 493 /** 494 * Returns the dimensions of the 3D object. 495 * 496 * @return The dimensions (never {@code null}). 497 */ 498 @Override 499 public Dimension3D getDimensions() { 500 return this.plot.getDimensions(); 501 } 502 503 /** 504 * Returns the view point. 505 * 506 * @return The view point (never {@code null}). 507 */ 508 @Override 509 public ViewPoint3D getViewPoint() { 510 return this.viewPoint; 511 } 512 513 /** 514 * Sets the view point. 515 * 516 * @param viewPoint the view point ({@code null} not permitted). 517 */ 518 @Override 519 public void setViewPoint(ViewPoint3D viewPoint) { 520 ArgChecks.nullNotPermitted(viewPoint, "viewPoint"); 521 this.viewPoint = viewPoint; 522 fireChangeEvent(); 523 } 524 525 /** 526 * Returns the projection distance. The default value is 527 * {@link #DEFAULT_PROJ_DIST}, higher numbers flatten out the perspective 528 * and reduce distortion in the projected image. 529 * 530 * @return The projection distance. 531 * 532 * @since 1.2 533 */ 534 @Override 535 public double getProjDistance() { 536 return this.projDist; 537 } 538 539 /** 540 * Sets the projection distance and sends a change event to all registered 541 * listeners. 542 * 543 * @param dist the distance. 544 * 545 * @since 1.2 546 */ 547 @Override 548 public void setProjDistance(double dist) { 549 this.projDist = dist; 550 fireChangeEvent(); 551 } 552 553 /** 554 * Sets the offset in 2D-space for the rendering of the chart. The 555 * default value is {@code (0, 0)} but the user can modify it via 556 * ALT-mouse-drag in the chart panel, providing an easy way to get improved 557 * chart alignment in the panels (especially prior to export to PNG, SVG or 558 * PDF). 559 * 560 * @return The offset (never {@code null}). 561 */ 562 @Override 563 public Offset2D getTranslate2D() { 564 return this.translate2D; 565 } 566 567 /** 568 * Sets the offset in 2D-space for the rendering of the chart and sends a 569 * change event to all registered listeners. 570 * 571 * @param offset the new offset ({@code null} not permitted). 572 */ 573 @Override 574 public void setTranslate2D(Offset2D offset) { 575 ArgChecks.nullNotPermitted(offset, "offset"); 576 this.translate2D = offset; 577 fireChangeEvent(); 578 } 579 580 /** 581 * Returns the legend builder. The default value is an instance of 582 * {@link StandardLegendBuilder}. If the legend builder is {@code null}, 583 * no legend will be displayed for the chart. 584 * 585 * @return The legend builder (possibly {@code null}). 586 * 587 * @see #setLegendBuilder(com.orsoncharts.legend.LegendBuilder) 588 * @see #setLegendAnchor(com.orsoncharts.util.Anchor2D) 589 */ 590 public LegendBuilder getLegendBuilder() { 591 return this.legendBuilder; 592 } 593 594 /** 595 * Sets the legend builder and sends a change event to all registered 596 * listeners. When the legend builder is {@code null}, no legend 597 * will be displayed on the chart. 598 * 599 * @param legendBuilder the legend builder ({@code null} permitted). 600 * 601 * @see #setLegendAnchor(com.orsoncharts.util.Anchor2D) 602 */ 603 public void setLegendBuilder(LegendBuilder legendBuilder) { 604 this.legendBuilder = legendBuilder; 605 fireChangeEvent(); 606 } 607 608 /** 609 * Returns the legend anchor. 610 * 611 * @return The legend anchor (never {@code null}). 612 * 613 * @see #setLegendAnchor(com.orsoncharts.util.Anchor2D) 614 */ 615 public Anchor2D getLegendAnchor() { 616 return this.legendAnchor; 617 } 618 619 /** 620 * Sets the legend anchor and sends a {@link Chart3DChangeEvent} to all 621 * registered listeners. There is a {@link LegendAnchor} class providing 622 * some useful default anchors. 623 * 624 * @param anchor the anchor ({@code null} not permitted). 625 * 626 * @see #getLegendAnchor() 627 */ 628 public void setLegendAnchor(Anchor2D anchor) { 629 ArgChecks.nullNotPermitted(anchor, "anchor"); 630 this.legendAnchor = anchor; 631 fireChangeEvent(); 632 } 633 634 /** 635 * Returns the orientation for the legend. 636 * 637 * @return The orientation (never {@code null}). 638 * 639 * @since 1.1 640 */ 641 public Orientation getLegendOrientation() { 642 return this.legendOrientation; 643 } 644 645 /** 646 * Sets the legend orientation and sends a {@link Chart3DChangeEvent} 647 * to all registered listeners. 648 * 649 * @param orientation the orientation ({@code null} not permitted). 650 * 651 * @since 1.1 652 */ 653 public void setLegendOrientation(Orientation orientation) { 654 ArgChecks.nullNotPermitted(orientation, "orientation"); 655 this.legendOrientation = orientation; 656 fireChangeEvent(); 657 } 658 659 /** 660 * Sets the legend position (both the anchor point and the orientation) and 661 * sends a {@link Chart3DChangeEvent} to all registered listeners. 662 * This is a convenience method that calls both the 663 * {@link #setLegendAnchor(com.orsoncharts.util.Anchor2D)} and 664 * {@link #setLegendOrientation(com.orsoncharts.util.Orientation)} 665 * methods. 666 * 667 * @param anchor the anchor ({@code null} not permitted). 668 * @param orientation the orientation ({@code null} not permitted). 669 * 670 * @since 1.1 671 */ 672 public void setLegendPosition(Anchor2D anchor, Orientation orientation) { 673 setNotify(false); 674 setLegendAnchor(anchor); 675 setLegendOrientation(orientation); 676 setNotify(true); 677 } 678 679 /** 680 * Returns the collection of rendering hints for the chart. 681 * 682 * @return The rendering hints for the chart (never {@code null}). 683 * 684 * @see #setRenderingHints(RenderingHints) 685 * 686 * @since 1.1 687 */ 688 public RenderingHints getRenderingHints() { 689 return this.renderingHints; 690 } 691 692 /** 693 * Sets the rendering hints for the chart. These will be added (using the 694 * {@code Graphics2D.addRenderingHints()} method) near the start of 695 * the chart rendering. Note that calling this method will replace all 696 * existing hints assigned to the chart. If you simply wish to add an 697 * additional hint, you can use {@code getRenderingHints().put(key, value)}. 698 * 699 * @param hints the rendering hints ({@code null} not permitted). 700 * 701 * @see #getRenderingHints() 702 * 703 * @since 1.1 704 */ 705 public void setRenderingHints(RenderingHints hints) { 706 ArgChecks.nullNotPermitted(hints, "hints"); 707 this.renderingHints = hints; 708 fireChangeEvent(); 709 } 710 711 /** 712 * Returns a flag that indicates whether or not anti-aliasing is used when 713 * the chart is drawn. 714 * 715 * @return The flag. 716 * 717 * @see #setAntiAlias(boolean) 718 * @since 1.1 719 */ 720 public boolean getAntiAlias() { 721 Object val = this.renderingHints.get(RenderingHints.KEY_ANTIALIASING); 722 return RenderingHints.VALUE_ANTIALIAS_ON.equals(val); 723 } 724 725 /** 726 * Sets a flag that indicates whether or not anti-aliasing is used when the 727 * chart is drawn. 728 * <P> 729 * Anti-aliasing usually improves the appearance of charts, but is slower. 730 * 731 * @param flag the new value of the flag. 732 * 733 * @see #getAntiAlias() 734 * @since 1.1 735 */ 736 public void setAntiAlias(boolean flag) { 737 if (flag) { 738 this.renderingHints.put(RenderingHints.KEY_ANTIALIASING, 739 RenderingHints.VALUE_ANTIALIAS_ON); 740 } else { 741 this.renderingHints.put(RenderingHints.KEY_ANTIALIASING, 742 RenderingHints.VALUE_ANTIALIAS_OFF); 743 } 744 fireChangeEvent(); 745 } 746 747 /** 748 * Returns the flag that controls whether or not element hints will be 749 * added to the {@code Graphics2D} output when the chart is rendered. 750 * The default value is {@code false}. 751 * 752 * @return A boolean. 753 * 754 * @since 1.3 755 */ 756 public boolean getElementHinting() { 757 return this.elementHinting; 758 } 759 760 /** 761 * Sets the flag that controls whether or not element hints will be 762 * added to the {@code Graphics2D} output when the chart is rendered 763 * and sends a change event to all registered listeners. 764 * 765 * @param hinting the new flag value. 766 * 767 * @since 1.3 768 */ 769 public void setElementHinting(boolean hinting) { 770 this.elementHinting = hinting; 771 fireChangeEvent(); 772 } 773 774 /** 775 * Returns the chart style. 776 * 777 * @return The chart style (never {@code null}). 778 * 779 * @since 1.2 780 */ 781 public ChartStyle getStyle() { 782 return this.style; 783 } 784 785 /** 786 * Sets (and applies) the specified chart style. 787 * 788 * @param style the chart style ({@code null} not permitted). 789 * 790 * @since 1.2 791 */ 792 public void setStyle(ChartStyle style) { 793 ArgChecks.nullNotPermitted(style, "style"); 794 this.style.removeChangeListener(this); 795 this.style = style; 796 this.style.addChangeListener(this); 797 setNotify(false); 798 receive(new ChartStyler(this.style)); 799 setNotify(true); 800 } 801 802 /** 803 * Creates a world containing the chart and the supplied chart box. 804 * 805 * @param chartBox the chart box ({@code null} permitted). 806 */ 807 private World createWorld(ChartBox3D chartBox) { 808 World result = new World(); 809 Dimension3D dim = this.plot.getDimensions(); 810 double w = dim.getWidth(); 811 double h = dim.getHeight(); 812 double d = dim.getDepth(); 813 if (chartBox != null) { 814 result.add("chartbox", chartBox.createObject3D()); 815 } 816 this.plot.compose(result, -w / 2, -h / 2, -d / 2); 817 return result; 818 } 819 820 /** 821 * Draws the chart to the specified output target. 822 * 823 * @param g2 the output target ({@code null} not permitted). 824 * 825 * @return Information about the items rendered. 826 */ 827 @Override 828 public RenderingInfo draw(Graphics2D g2, Rectangle2D bounds) { 829 beginElement(g2, this.id, "ORSON_CHART_TOP_LEVEL"); 830 Shape savedClip = g2.getClip(); 831 g2.clip(bounds); 832 g2.addRenderingHints(this.renderingHints); 833 g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, 834 BasicStroke.JOIN_ROUND, 1f)); 835 Dimension3D dim3D = this.plot.getDimensions(); 836 double w = dim3D.getWidth(); 837 double h = dim3D.getHeight(); 838 double depth = dim3D.getDepth(); 839 ChartBox3D chartBox = null; 840 if (this.plot instanceof XYZPlot 841 || this.plot instanceof CategoryPlot3D) { 842 double[] tickUnits = findAxisTickUnits(g2, w, h, depth); 843 chartBox = new ChartBox3D(w, h, depth, -w / 2, -h / 2, -depth / 2, 844 this.chartBoxColor); 845 chartBox.setXTicks(fetchXTickData(this.plot, tickUnits[0])); 846 chartBox.setYTicks(fetchYTickData(this.plot, tickUnits[1])); 847 chartBox.setZTicks(fetchZTickData(this.plot, tickUnits[2])); 848 chartBox.setXMarkers(fetchXMarkerData(this.plot)); 849 chartBox.setYMarkers(fetchYMarkerData(this.plot)); 850 chartBox.setZMarkers(fetchZMarkerData(this.plot)); 851 } 852 if (this.world == null) { 853 this.world = createWorld(chartBox); 854 } else if (chartBox != null) { 855 this.world.clear("chartbox"); 856 this.world.add("chartbox", chartBox.createObject3D()); 857 } 858 if (this.background != null) { 859 this.background.fill(g2, bounds); 860 } 861 AffineTransform saved = g2.getTransform(); 862 double dx = bounds.getX() + bounds.getWidth() / 2.0 863 + this.translate2D.getDX(); 864 double dy = bounds.getY() + bounds.getHeight() / 2.0 865 + this.translate2D.getDY(); 866 g2.translate(dx, dy); 867 Point3D[] eyePts = this.world.calculateEyeCoordinates(this.viewPoint); 868 Point2D[] pts = this.world.calculateProjectedPoints(this.viewPoint, 869 this.projDist); 870 871 // sort faces by z-order 872 List<Face> facesInPaintOrder = new ArrayList<Face>(world.getFaces()); 873 facesInPaintOrder = this.faceSorter.sort(facesInPaintOrder, eyePts); 874 Line2D line = null; 875 Stroke stroke = new BasicStroke(1.0f); 876 for (Face f : facesInPaintOrder) { 877 // check for the special case where the face is just a line 878 if (f.getVertexCount() == 2) { 879 g2.setPaint(f.getColor()); 880 if (line == null) { 881 line = new Line2D.Float(); 882 } 883 int v0 = f.getVertexIndex(0); 884 int v1 = f.getVertexIndex(1); 885 line.setLine(pts[v0].getX(), pts[v0].getY(), pts[v1].getX(), 886 pts[v1].getY()); 887 g2.setStroke(stroke); 888 g2.draw(line); 889 continue; 890 } 891 boolean drawOutline = f.getOutline(); 892 double[] plane = f.calculateNormal(eyePts); 893 double inprod = plane[0] * world.getSunX() + plane[1] 894 * world.getSunY() + plane[2] * world.getSunZ(); 895 double shade = (inprod + 1) / 2.0; 896 if (f instanceof DoubleSidedFace 897 || Utils2D.area2(pts[f.getVertexIndex(0)], 898 pts[f.getVertexIndex(1)], pts[f.getVertexIndex(2)]) > 0.0) { 899 Color c = f.getColor(); 900 Path2D p = f.createPath(pts); 901 g2.setPaint(new Color((int) (c.getRed() * shade), 902 (int) (c.getGreen() * shade), 903 (int) (c.getBlue() * shade), c.getAlpha())); 904 if (this.elementHinting) { 905 beginElementGroup(f, g2); 906 } 907 g2.fill(p); 908 if (drawOutline) { 909 g2.draw(p); 910 } 911 if (this.elementHinting) { 912 endElementGroup(f, g2); 913 } 914 915 if (f instanceof ChartBoxFace 916 && (this.plot instanceof CategoryPlot3D 917 || this.plot instanceof XYZPlot)) { 918 Stroke savedStroke = g2.getStroke(); 919 ChartBoxFace cbf = (ChartBoxFace) f; 920 drawGridlines(g2, cbf, pts); 921 drawMarkers(g2, cbf, pts); 922 g2.setStroke(savedStroke); 923 } 924 } else if (f instanceof LabelFace) { 925 LabelFace lf = (LabelFace) f; 926 Path2D p = lf.createPath(pts); 927 Rectangle2D lb = p.getBounds2D(); 928 g2.setFont(lf.getFont()); 929 g2.setColor(lf.getBackgroundColor()); 930 Rectangle2D bb = TextUtils.calcAlignedStringBounds( 931 lf.getLabel(), g2, 932 (float) lb.getCenterX(), (float) lb.getCenterY(), 933 TextAnchor.CENTER); 934 g2.fill(bb); 935 g2.setColor(lf.getTextColor()); 936 Rectangle2D r = TextUtils.drawAlignedString(lf.getLabel(), g2, 937 (float) lb.getCenterX(), (float) lb.getCenterY(), 938 TextAnchor.CENTER); 939 lf.getOwner().setProperty("labelBounds", r); 940 } 941 } 942 RenderingInfo info = new RenderingInfo(facesInPaintOrder, pts, dx, dy); 943 OnDrawHandler onDrawHandler = new OnDrawHandler(info, 944 this.elementHinting); 945 946 // handle labels on pie plots... 947 if (this.plot instanceof PiePlot3D) { 948 drawPieLabels(g2, w, h, depth, info); 949 } 950 951 // handle axis labelling on non-pie plots... 952 if (this.plot instanceof XYZPlot || this.plot instanceof 953 CategoryPlot3D) { 954 drawAxes(g2, chartBox, pts, info); 955 } 956 957 g2.setTransform(saved); 958 959 // generate and draw the legend... 960 if (this.legendBuilder != null) { 961 TableElement legend = this.legendBuilder.createLegend(this.plot, 962 this.legendAnchor, this.legendOrientation, this.style); 963 if (legend != null) { 964 Dimension2D legendSize = legend.preferredSize(g2, bounds); 965 Rectangle2D legendArea = calculateDrawArea(legendSize, 966 this.legendAnchor, bounds); 967 legend.draw(g2, legendArea, onDrawHandler); 968 } 969 } 970 971 // draw the title... 972 if (this.title != null) { 973 Dimension2D titleSize = this.title.preferredSize(g2, bounds); 974 Rectangle2D titleArea = calculateDrawArea(titleSize, 975 this.titleAnchor, bounds); 976 this.title.draw(g2, titleArea, onDrawHandler); 977 } 978 g2.setClip(savedClip); 979 endElement(g2); 980 return info; 981 } 982 983 private void beginElementGroup(Face face, Graphics2D g2) { 984 Object3D owner = face.getOwner(); 985 ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY); 986 if (itemKey != null) { 987 Map m = new HashMap(); 988 m.put("ref", itemKey.toJSONString()); 989 g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); 990 } 991 } 992 993 private void endElementGroup(Face face, Graphics2D g2) { 994 Object3D owner = face.getOwner(); 995 ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY); 996 if (itemKey != null) { 997 g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE); 998 } 999 } 1000 1001 /** 1002 * An implementation method that fetches x-axis tick data from the plot, 1003 * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}. 1004 * On a category plot, the x-axis is the column axis (and the tickUnit is 1005 * ignored). 1006 * 1007 * @param plot the plot. 1008 * @param tickUnit the tick unit. 1009 * 1010 * @return A list of tick data instances representing the tick marks and 1011 * values along the x-axis. 1012 */ 1013 private List<TickData> fetchXTickData(Plot3D plot, double tickUnit) { 1014 if (plot instanceof CategoryPlot3D) { 1015 CategoryPlot3D cp = (CategoryPlot3D) plot; 1016 return cp.getColumnAxis().generateTickDataForColumns( 1017 cp.getDataset()); 1018 } 1019 if (plot instanceof XYZPlot) { 1020 XYZPlot xp = (XYZPlot) plot; 1021 return xp.getXAxis().generateTickData(tickUnit); 1022 } 1023 return Collections.emptyList(); 1024 } 1025 1026 /** 1027 * An implementation method that fetches y-axis tick data from the plot, 1028 * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}. 1029 * On a category plot, the y-axis is the value axis. 1030 * 1031 * @param plot the plot. 1032 * @param tickUnit the tick unit. 1033 * 1034 * @return A list of tick data instances representing the tick marks and 1035 * values along the y-axis. 1036 */ 1037 private List<TickData> fetchYTickData(Plot3D plot, double tickUnit) { 1038 if (plot instanceof CategoryPlot3D) { 1039 CategoryPlot3D cp = (CategoryPlot3D) plot; 1040 return cp.getValueAxis().generateTickData(tickUnit); 1041 } 1042 if (plot instanceof XYZPlot) { 1043 XYZPlot xp = (XYZPlot) plot; 1044 return xp.getYAxis().generateTickData(tickUnit); 1045 } 1046 return Collections.emptyList(); 1047 } 1048 1049 /** 1050 * An implementation method that fetches z-axis tick data from the plot, 1051 * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}. 1052 * On a category plot, the z-axis is the row axis (and the tickUnit is 1053 * ignored). 1054 * 1055 * @param plot the plot. 1056 * @param tickUnit the tick unit. 1057 * 1058 * @return A list of tick data instances representing the tick marks and 1059 * values along the y-axis. 1060 */ 1061 private List<TickData> fetchZTickData(Plot3D plot, double tickUnit) { 1062 if (plot instanceof CategoryPlot3D) { 1063 CategoryPlot3D cp = (CategoryPlot3D) plot; 1064 return cp.getRowAxis().generateTickDataForRows(cp.getDataset()); 1065 } 1066 if (plot instanceof XYZPlot) { 1067 XYZPlot xp = (XYZPlot) plot; 1068 return xp.getZAxis().generateTickData(tickUnit); 1069 } 1070 return Collections.emptyList(); 1071 } 1072 1073 /** 1074 * Fetches marker data for the plot's x-axis. 1075 * 1076 * @param plot the plot ({@code null} not permitted). 1077 * 1078 * @return A list of marker data items (possibly empty but never 1079 * {@code null}). 1080 */ 1081 private List<MarkerData> fetchXMarkerData(Plot3D plot) { 1082 if (plot instanceof CategoryPlot3D) { 1083 return ((CategoryPlot3D) plot).getColumnAxis().generateMarkerData(); 1084 } 1085 if (plot instanceof XYZPlot) { 1086 return ((XYZPlot) plot).getXAxis().generateMarkerData(); 1087 } 1088 return new ArrayList<MarkerData>(0); 1089 } 1090 1091 /** 1092 * Fetches marker data for the plot's x-axis. 1093 * 1094 * @param plot the plot ({@code null} not permitted). 1095 * 1096 * @return A list of marker data items (possibly empty but never 1097 * {@code null}). 1098 */ 1099 private List<MarkerData> fetchYMarkerData(Plot3D plot) { 1100 if (plot instanceof CategoryPlot3D) { 1101 return ((CategoryPlot3D) plot).getValueAxis().generateMarkerData(); 1102 } 1103 if (plot instanceof XYZPlot) { 1104 return ((XYZPlot) plot).getYAxis().generateMarkerData(); 1105 } 1106 return new ArrayList<MarkerData>(0); 1107 } 1108 1109 /** 1110 * Fetches marker data for the plot's x-axis. 1111 * 1112 * @param plot the plot ({@code null} not permitted). 1113 * 1114 * @return A list of marker data items (possibly empty but never 1115 * {@code null}). 1116 */ 1117 private List<MarkerData> fetchZMarkerData(Plot3D plot) { 1118 if (plot instanceof CategoryPlot3D) { 1119 return ((CategoryPlot3D) plot).getRowAxis().generateMarkerData(); 1120 } 1121 if (plot instanceof XYZPlot) { 1122 return ((XYZPlot) plot).getZAxis().generateMarkerData(); 1123 } 1124 return new ArrayList<MarkerData>(0); 1125 } 1126 1127 /** 1128 * Draw the gridlines for one chart box face. 1129 * 1130 * @param g2 the graphics target. 1131 * @param face the face. 1132 * @param pts the projection points. 1133 */ 1134 private void drawGridlines(Graphics2D g2, ChartBoxFace face, 1135 Point2D[] pts) { 1136 if (isGridlinesVisibleForX(this.plot)) { 1137 g2.setPaint(fetchGridlinePaintX(this.plot)); 1138 g2.setStroke(fetchGridlineStrokeX(this.plot)); 1139 List<TickData> xA = face.getXTicksA(); 1140 List<TickData> xB = face.getXTicksB(); 1141 for (int i = 0; i < xA.size(); i++) { 1142 Line2D line = new Line2D.Double( 1143 pts[face.getOffset() + xA.get(i).getVertexIndex()], 1144 pts[face.getOffset() + xB.get(i).getVertexIndex()]); 1145 g2.draw(line); 1146 } 1147 } 1148 1149 if (isGridlinesVisibleForY(this.plot)) { 1150 g2.setPaint(fetchGridlinePaintY(this.plot)); 1151 g2.setStroke(fetchGridlineStrokeY(this.plot)); 1152 List<TickData> yA = face.getYTicksA(); 1153 List<TickData> yB = face.getYTicksB(); 1154 for (int i = 0; i < yA.size(); i++) { 1155 Line2D line = new Line2D.Double( 1156 pts[face.getOffset() + yA.get(i).getVertexIndex()], 1157 pts[face.getOffset() + yB.get(i).getVertexIndex()]); 1158 g2.draw(line); 1159 } 1160 } 1161 1162 if (isGridlinesVisibleForZ(this.plot)) { 1163 g2.setPaint(fetchGridlinePaintZ(this.plot)); 1164 g2.setStroke(fetchGridlineStrokeZ(this.plot)); 1165 List<TickData> zA = face.getZTicksA(); 1166 List<TickData> zB = face.getZTicksB(); 1167 for (int i = 0; i < zA.size(); i++) { 1168 Line2D line = new Line2D.Double( 1169 pts[face.getOffset() + zA.get(i).getVertexIndex()], 1170 pts[face.getOffset() + zB.get(i).getVertexIndex()]); 1171 g2.draw(line); 1172 } 1173 } 1174 } 1175 1176 /** 1177 * Returns {@code true} if gridlines are visible for the x-axis 1178 * (column axis in the case of a {@link CategoryPlot3D}) and 1179 * {@code false} otherwise. For pie charts, this method will always 1180 * return {@code false}. 1181 * 1182 * @param plot the plot. 1183 * 1184 * @return A boolean. 1185 */ 1186 private boolean isGridlinesVisibleForX(Plot3D plot) { 1187 if (plot instanceof CategoryPlot3D) { 1188 CategoryPlot3D cp = (CategoryPlot3D) plot; 1189 return cp.getGridlinesVisibleForColumns(); 1190 } 1191 if (plot instanceof XYZPlot) { 1192 XYZPlot xp = (XYZPlot) plot; 1193 return xp.isGridlinesVisibleX(); 1194 } 1195 return false; 1196 } 1197 1198 /** 1199 * Returns {@code true} if gridlines are visible for the y-axis 1200 * (value axis in the case of a {@link CategoryPlot3D}) and 1201 * {@code false} otherwise. 1202 * 1203 * @param plot the plot. 1204 * 1205 * @return A boolean. 1206 */ 1207 private boolean isGridlinesVisibleForY(Plot3D plot) { 1208 if (plot instanceof CategoryPlot3D) { 1209 CategoryPlot3D cp = (CategoryPlot3D) plot; 1210 return cp.getGridlinesVisibleForValues(); 1211 } 1212 if (plot instanceof XYZPlot) { 1213 XYZPlot xp = (XYZPlot) plot; 1214 return xp.isGridlinesVisibleY(); 1215 } 1216 return false; 1217 } 1218 1219 /** 1220 * Returns {@code true} if gridlines are visible for the z-axis 1221 * (row axis in the case of a {@link CategoryPlot3D}) and 1222 * {@code false} otherwise. 1223 * 1224 * @param plot the plot. 1225 * 1226 * @return A boolean. 1227 */ 1228 private boolean isGridlinesVisibleForZ(Plot3D plot) { 1229 if (plot instanceof CategoryPlot3D) { 1230 CategoryPlot3D cp = (CategoryPlot3D) plot; 1231 return cp.getGridlinesVisibleForRows(); 1232 } 1233 if (plot instanceof XYZPlot) { 1234 XYZPlot xp = (XYZPlot) plot; 1235 return xp.isGridlinesVisibleZ(); 1236 } 1237 return false; 1238 } 1239 1240 /** 1241 * Returns the paint used to draw gridlines on the x-axis (or column axis 1242 * in the case of {@link CategoryPlot3D}). 1243 * 1244 * @param plot the plot. 1245 * 1246 * @return The paint. 1247 */ 1248 private Paint fetchGridlinePaintX(Plot3D plot) { 1249 if (plot instanceof CategoryPlot3D) { 1250 CategoryPlot3D cp = (CategoryPlot3D) plot; 1251 return cp.getGridlinePaintForColumns(); 1252 } 1253 if (plot instanceof XYZPlot) { 1254 XYZPlot xp = (XYZPlot) plot; 1255 return xp.getGridlinePaintX(); 1256 } 1257 return null; 1258 } 1259 1260 /** 1261 * Returns the paint used to draw gridlines on the y-axis (or value axis 1262 * in the case of {@link CategoryPlot3D}). 1263 * 1264 * @param plot the plot. 1265 * 1266 * @return The paint. 1267 */ 1268 private Paint fetchGridlinePaintY(Plot3D plot) { 1269 if (plot instanceof CategoryPlot3D) { 1270 CategoryPlot3D cp = (CategoryPlot3D) plot; 1271 return cp.getGridlinePaintForValues(); 1272 } 1273 if (plot instanceof XYZPlot) { 1274 XYZPlot xp = (XYZPlot) plot; 1275 return xp.getGridlinePaintY(); 1276 } 1277 return null; 1278 } 1279 1280 /** 1281 * Returns the paint used to draw gridlines on the z-axis (or row axis 1282 * in the case of {@link CategoryPlot3D}). 1283 * 1284 * @param plot the plot. 1285 * 1286 * @return The paint. 1287 */ 1288 private Paint fetchGridlinePaintZ(Plot3D plot) { 1289 if (plot instanceof CategoryPlot3D) { 1290 CategoryPlot3D cp = (CategoryPlot3D) plot; 1291 return cp.getGridlinePaintForRows(); 1292 } 1293 if (plot instanceof XYZPlot) { 1294 XYZPlot xp = (XYZPlot) plot; 1295 return xp.getGridlinePaintZ(); 1296 } 1297 return null; 1298 } 1299 1300 /** 1301 * Returns the stroke used to draw gridlines on the x-axis (or column axis 1302 * in the case of {@link CategoryPlot3D}). 1303 * 1304 * @param plot the plot. 1305 * 1306 * @return The stroke. 1307 */ 1308 private Stroke fetchGridlineStrokeX(Plot3D plot) { 1309 if (plot instanceof CategoryPlot3D) { 1310 CategoryPlot3D cp = (CategoryPlot3D) plot; 1311 return cp.getGridlineStrokeForColumns(); 1312 } 1313 if (plot instanceof XYZPlot) { 1314 XYZPlot xp = (XYZPlot) plot; 1315 return xp.getGridlineStrokeX(); 1316 } 1317 return null; 1318 } 1319 1320 /** 1321 * Returns the stroke used to draw gridlines on the y-axis (or value axis 1322 * in the case of {@link CategoryPlot3D}). 1323 * 1324 * @param plot the plot. 1325 * 1326 * @return The stroke. 1327 */ 1328 private Stroke fetchGridlineStrokeY(Plot3D plot) { 1329 if (plot instanceof CategoryPlot3D) { 1330 CategoryPlot3D cp = (CategoryPlot3D) plot; 1331 return cp.getGridlineStrokeForValues(); 1332 } 1333 if (plot instanceof XYZPlot) { 1334 XYZPlot xp = (XYZPlot) plot; 1335 return xp.getGridlineStrokeY(); 1336 } 1337 return null; 1338 } 1339 1340 /** 1341 * Returns the stroke used to draw gridlines on the z-axis (or row axis 1342 * in the case of {@link CategoryPlot3D}). 1343 * 1344 * @param plot the plot. 1345 * 1346 * @return The stroke. 1347 */ 1348 private Stroke fetchGridlineStrokeZ(Plot3D plot) { 1349 if (plot instanceof CategoryPlot3D) { 1350 CategoryPlot3D cp = (CategoryPlot3D) plot; 1351 return cp.getGridlineStrokeForRows(); 1352 } 1353 if (plot instanceof XYZPlot) { 1354 XYZPlot xp = (XYZPlot) plot; 1355 return xp.getGridlineStrokeZ(); 1356 } 1357 return null; 1358 } 1359 1360 /** 1361 * Draws the pie labels for a {@link PiePlot3D} in 2D-space by creating a 1362 * temporary world with vertices at anchor points for the labels, then 1363 * projecting the points to 2D-space. 1364 * 1365 * @param g2 the graphics target. 1366 * @param w the width. 1367 * @param h the height. 1368 * @param depth the depth. 1369 * @param info the rendering info ({@code null} permitted). 1370 */ 1371 private void drawPieLabels(Graphics2D g2, double w, double h, 1372 double depth, RenderingInfo info) { 1373 PiePlot3D p = (PiePlot3D) this.plot; 1374 World labelOverlay = new World(); 1375 List<Object3D> objs = p.getLabelFaces(-w / 2, -h / 2, -depth / 2); 1376 for (Object3D obj : objs) { 1377 labelOverlay.add(obj); 1378 } 1379 Point2D[] ppts = labelOverlay.calculateProjectedPoints( 1380 this.viewPoint, this.projDist); 1381 for (int i = 0; i < p.getDataset().getItemCount() * 2; i++) { 1382 if (p.getDataset().getValue(i / 2) == null) { 1383 continue; 1384 } 1385 Face f = labelOverlay.getFaces().get(i); 1386 if (Utils2D.area2(ppts[f.getVertexIndex(0)], 1387 ppts[f.getVertexIndex(1)], 1388 ppts[f.getVertexIndex(2)]) > 0) { 1389 Comparable<?> key = p.getDataset().getKey(i / 2); 1390 g2.setColor(p.getSectionLabelColorSource().getColor(key)); 1391 g2.setFont(p.getSectionLabelFontSource().getFont(key)); 1392 Point2D pt = Utils2D.centerPoint(ppts[f.getVertexIndex(0)], 1393 ppts[f.getVertexIndex(1)], ppts[f.getVertexIndex(2)], 1394 ppts[f.getVertexIndex(3)]); 1395 String label = p.getSectionLabelGenerator().generateLabel( 1396 p.getDataset(), key); 1397 String ref = "{\"type\": \"sectionLabel\", \"key\": \"" 1398 + key.toString() + "\"}"; 1399 beginElementWithRef(g2, ref); 1400 Rectangle2D bounds = TextUtils.drawAlignedString(label, g2, 1401 (float) pt.getX(), (float) pt.getY(), 1402 TextAnchor.CENTER); 1403 endElement(g2); 1404 1405 if (info != null) { 1406 RenderedElement pieLabelRE = new RenderedElement( 1407 InteractiveElementType.SECTION_LABEL, bounds); 1408 pieLabelRE.setProperty("key", key); 1409 info.addOffsetElement(pieLabelRE); 1410 } 1411 } 1412 } 1413 } 1414 1415 private void beginElementWithRef(Graphics2D g2, String ref) { 1416 beginElement(g2, null, ref); 1417 } 1418 1419 private void beginElement(Graphics2D g2, String id, String ref) { 1420 if (this.elementHinting) { 1421 Map m = new HashMap<String, String>(); 1422 if (id != null) { 1423 m.put("id", id); 1424 } 1425 m.put("ref", ref); 1426 g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); 1427 } 1428 } 1429 1430 private void endElement(Graphics2D g2) { 1431 if (this.elementHinting) { 1432 g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE); 1433 } 1434 } 1435 1436 /** 1437 * Determines appropriate tick units for the axes in the chart. 1438 * 1439 * @param g2 the graphics target. 1440 * @param w the width. 1441 * @param h the height. 1442 * @param depth the depth. 1443 * 1444 * @return The tick sizes. 1445 */ 1446 private double[] findAxisTickUnits(Graphics2D g2, double w, double h, 1447 double depth) { 1448 World tempWorld = new World(); 1449 ChartBox3D chartBox = new ChartBox3D(w, h, depth, -w / 2.0, -h / 2.0, 1450 -depth / 2.0, Color.WHITE); 1451 tempWorld.add(chartBox.createObject3D()); 1452 Point2D[] axisPts2D = tempWorld.calculateProjectedPoints( 1453 this.viewPoint, this.projDist); 1454 1455 // vertices 1456 Point2D v0 = axisPts2D[0]; 1457 Point2D v1 = axisPts2D[1]; 1458 Point2D v2 = axisPts2D[2]; 1459 Point2D v3 = axisPts2D[3]; 1460 Point2D v4 = axisPts2D[4]; 1461 Point2D v5 = axisPts2D[5]; 1462 Point2D v6 = axisPts2D[6]; 1463 Point2D v7 = axisPts2D[7]; 1464 1465 // faces 1466 boolean a = chartBox.faceA().isFrontFacing(axisPts2D); 1467 boolean b = chartBox.faceB().isFrontFacing(axisPts2D); 1468 boolean c = chartBox.faceC().isFrontFacing(axisPts2D); 1469 boolean d = chartBox.faceD().isFrontFacing(axisPts2D); 1470 boolean e = chartBox.faceE().isFrontFacing(axisPts2D); 1471 boolean f = chartBox.faceF().isFrontFacing(axisPts2D); 1472 1473 double xtick = 0, ytick = 0, ztick = 0; 1474 Axis3D xAxis = null; 1475 ValueAxis3D yAxis = null; 1476 Axis3D zAxis = null; 1477 if (this.plot instanceof XYZPlot) { 1478 XYZPlot pp = (XYZPlot) this.plot; 1479 xAxis = pp.getXAxis(); 1480 yAxis = pp.getYAxis(); 1481 zAxis = pp.getZAxis(); 1482 } else if (this.plot instanceof CategoryPlot3D) { 1483 CategoryPlot3D pp = (CategoryPlot3D) this.plot; 1484 xAxis = pp.getColumnAxis(); 1485 yAxis = pp.getValueAxis(); 1486 zAxis = pp.getRowAxis(); 1487 } 1488 1489 if (xAxis != null && yAxis != null && zAxis != null) { 1490 double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0); 1491 double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0); 1492 double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0); 1493 double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0); 1494 double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0); 1495 double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0); 1496 double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0); 1497 double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0); 1498 double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0); 1499 double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0); 1500 double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0); 1501 double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0); 1502 1503 if (count(a, b) == 1 && longest(ab, bc, cd, da)) { 1504 if (xAxis instanceof ValueAxis3D) { 1505 xtick = ((ValueAxis3D) xAxis).selectTick(g2, v0, v1, v7); 1506 } 1507 } 1508 if (count(b, c) == 1 && longest(bc, ab, cd, da)) { 1509 if (xAxis instanceof ValueAxis3D) { 1510 xtick = ((ValueAxis3D) xAxis).selectTick(g2, v3, v2, v6); 1511 } 1512 } 1513 if (count(c, d) == 1 && longest(cd, ab, bc, da)) { 1514 if (xAxis instanceof ValueAxis3D) { 1515 xtick = ((ValueAxis3D) xAxis).selectTick(g2, v4, v7, v1); 1516 } 1517 } 1518 if (count(d, a) == 1 && longest(da, ab, bc, cd)) { 1519 if (xAxis instanceof ValueAxis3D) { 1520 xtick = ((ValueAxis3D) xAxis).selectTick(g2, v5, v6, v3); 1521 } 1522 } 1523 1524 if (count(b, e) == 1 && longest(be, bf, df, de)) { 1525 ytick = yAxis.selectTick(g2, v0, v3, v7); 1526 } 1527 if (count(b, f) == 1 && longest(bf, be, df, de)) { 1528 ytick = yAxis.selectTick(g2, v1, v2, v4); 1529 } 1530 if (count(d, f) == 1 && longest(df, be, bf, de)) { 1531 ytick = yAxis.selectTick(g2, v6, v7, v0); 1532 } 1533 if (count(d, e) == 1 && longest(de, be, bf, df)) { 1534 ytick = yAxis.selectTick(g2, v5, v4, v1); 1535 } 1536 1537 if (count(a, e) == 1 && longest(ae, af, cf, ce)) { 1538 if (zAxis instanceof ValueAxis3D) { 1539 ztick = ((ValueAxis3D) zAxis).selectTick(g2, v0, v5, v2); 1540 } 1541 } 1542 if (count(a, f) == 1 && longest(af, ae, cf, ce)) { 1543 if (zAxis instanceof ValueAxis3D) { 1544 ztick = ((ValueAxis3D) zAxis).selectTick(g2, v1, v6, v3); 1545 } 1546 } 1547 if (count(c, f) == 1 && longest(cf, ae, af, ce)) { 1548 if (zAxis instanceof ValueAxis3D) { 1549 ztick = ((ValueAxis3D) zAxis).selectTick(g2, v2, v7, v5); 1550 } 1551 } 1552 if (count(c, e) == 1 && longest(ce, ae, af, cf)) { 1553 if (zAxis instanceof ValueAxis3D) { 1554 ztick = ((ValueAxis3D) zAxis).selectTick(g2, v3, v4, v6); 1555 } 1556 } 1557 } 1558 return new double[] { xtick, ytick, ztick }; 1559 } 1560 1561 private void populateAnchorPoints(List<TickData> tickData, Point2D[] pts) { 1562 for (TickData t : tickData) { 1563 t.setAnchorPt(pts[t.getVertexIndex()]); 1564 } 1565 } 1566 1567 /** 1568 * Draws the axes for a chart. 1569 * 1570 * @param g2 the graphics target ({@code null} not permitted). 1571 * @param chartBox the chart box (this contains projected points for 1572 * the tick marks and labels) 1573 * @param pts the projected points. 1574 * @param info an object to be populated with rendering info, if it is 1575 * non-{@code null}. 1576 */ 1577 private void drawAxes(Graphics2D g2, ChartBox3D chartBox, Point2D[] pts, 1578 RenderingInfo info) { 1579 1580 // vertices 1581 Point2D v0 = pts[0]; 1582 Point2D v1 = pts[1]; 1583 Point2D v2 = pts[2]; 1584 Point2D v3 = pts[3]; 1585 Point2D v4 = pts[4]; 1586 Point2D v5 = pts[5]; 1587 Point2D v6 = pts[6]; 1588 Point2D v7 = pts[7]; 1589 1590 // faces 1591 boolean a = chartBox.faceA().isFrontFacing(pts); 1592 boolean b = chartBox.faceB().isFrontFacing(pts); 1593 boolean c = chartBox.faceC().isFrontFacing(pts); 1594 boolean d = chartBox.faceD().isFrontFacing(pts); 1595 boolean e = chartBox.faceE().isFrontFacing(pts); 1596 boolean f = chartBox.faceF().isFrontFacing(pts); 1597 1598 Axis3D xAxis = null, yAxis = null, zAxis = null; 1599 if (this.plot instanceof XYZPlot) { 1600 XYZPlot pp = (XYZPlot) this.plot; 1601 xAxis = pp.getXAxis(); 1602 yAxis = pp.getYAxis(); 1603 zAxis = pp.getZAxis(); 1604 } else if (this.plot instanceof CategoryPlot3D) { 1605 CategoryPlot3D pp = (CategoryPlot3D) this.plot; 1606 xAxis = pp.getColumnAxis(); 1607 yAxis = pp.getValueAxis(); 1608 zAxis = pp.getRowAxis(); 1609 } 1610 1611 if (xAxis != null && yAxis != null && zAxis != null) { 1612 double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0); 1613 double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0); 1614 double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0); 1615 double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0); 1616 double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0); 1617 double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0); 1618 double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0); 1619 double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0); 1620 double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0); 1621 double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0); 1622 double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0); 1623 double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0); 1624 1625 List<TickData> ticks; 1626 if (count(a, b) == 1 && longest(ab, bc, cd, da)) { 1627 ticks = chartBox.faceA().getXTicksA(); 1628 populateAnchorPoints(ticks, pts); 1629 xAxis.draw(g2, v0, v1, v7, ticks, info, this.elementHinting); 1630 } 1631 if (count(b, c) == 1 && longest(bc, ab, cd, da)) { 1632 ticks = chartBox.faceB().getXTicksB(); 1633 populateAnchorPoints(ticks, pts); 1634 xAxis.draw(g2, v3, v2, v6, ticks, info, this.elementHinting); 1635 } 1636 if (count(c, d) == 1 && longest(cd, ab, bc, da)) { 1637 ticks = chartBox.faceC().getXTicksB(); 1638 populateAnchorPoints(ticks, pts); 1639 xAxis.draw(g2, v4, v7, v1, ticks, info, this.elementHinting); 1640 } 1641 if (count(d, a) == 1 && longest(da, ab, bc, cd)) { 1642 ticks = chartBox.faceA().getXTicksB(); 1643 populateAnchorPoints(ticks, pts); 1644 xAxis.draw(g2, v5, v6, v3, ticks, info, this.elementHinting); 1645 } 1646 1647 if (count(b, e) == 1 && longest(be, bf, df, de)) { 1648 ticks = chartBox.faceB().getYTicksA(); 1649 populateAnchorPoints(ticks, pts); 1650 yAxis.draw(g2, v0, v3, v7, ticks, info, this.elementHinting); 1651 } 1652 if (count(b, f) == 1 && longest(bf, be, df, de)) { 1653 ticks = chartBox.faceB().getYTicksB(); 1654 populateAnchorPoints(ticks, pts); 1655 yAxis.draw(g2, v1, v2, v4, ticks, info, this.elementHinting); 1656 } 1657 if (count(d, f) == 1 && longest(df, be, bf, de)) { 1658 ticks = chartBox.faceD().getYTicksA(); 1659 populateAnchorPoints(ticks, pts); 1660 yAxis.draw(g2, v6, v7, v0, ticks, info, this.elementHinting); 1661 } 1662 if (count(d, e) == 1 && longest(de, be, bf, df)) { 1663 ticks = chartBox.faceD().getYTicksB(); 1664 populateAnchorPoints(ticks, pts); 1665 yAxis.draw(g2, v5, v4, v1, ticks, info, this.elementHinting); 1666 } 1667 1668 if (count(a, e) == 1 && longest(ae, af, cf, ce)) { 1669 ticks = chartBox.faceA().getZTicksA(); 1670 populateAnchorPoints(ticks, pts); 1671 zAxis.draw(g2, v0, v5, v2, ticks, info, this.elementHinting); 1672 } 1673 if (count(a, f) == 1 && longest(af, ae, cf, ce)) { 1674 ticks = chartBox.faceA().getZTicksB(); 1675 populateAnchorPoints(ticks, pts); 1676 zAxis.draw(g2, v1, v6, v3, ticks, info, this.elementHinting); 1677 } 1678 if (count(c, f) == 1 && longest(cf, ae, af, ce)) { 1679 ticks = chartBox.faceC().getZTicksB(); 1680 populateAnchorPoints(ticks, pts); 1681 zAxis.draw(g2, v2, v7, v5, ticks, info, this.elementHinting); 1682 } 1683 if (count(c, e) == 1 && longest(ce, ae, af, cf)) { 1684 ticks = chartBox.faceC().getZTicksA(); 1685 populateAnchorPoints(ticks, pts); 1686 zAxis.draw(g2, v3, v4, v6, ticks, info, this.elementHinting); 1687 } 1688 } 1689 } 1690 1691 /** 1692 * Draws the markers for one face on a chart box. The {@code pts} 1693 * array contains all the projected points for all the vertices in the 1694 * world...the chart box face references the required points by index. 1695 * 1696 * @param g2 the graphics target ({@code null} not permitted). 1697 * @param face the face of the chart box ({@code null} not permitted). 1698 * @param pts the projected points for the whole world. 1699 */ 1700 private void drawMarkers(Graphics2D g2, ChartBoxFace face, Point2D[] pts) { 1701 // x markers 1702 List<MarkerData> xmarkers = face.getXMarkers(); 1703 for (MarkerData m : xmarkers) { 1704 m.updateProjection(pts); 1705 Marker marker = fetchXMarker(this.plot, m.getMarkerKey()); 1706 beginElementWithRef(g2, "{\"type\": \"xMarker\", \"key\": \"" 1707 + m.getMarkerKey() + "\"}"); 1708 marker.draw(g2, m, true); 1709 endElement(g2); 1710 } 1711 1712 // y markers 1713 List<MarkerData> ymarkers = face.getYMarkers(); 1714 for (MarkerData m : ymarkers) { 1715 m.updateProjection(pts); 1716 Marker marker = fetchYMarker(this.plot, m.getMarkerKey()); 1717 beginElementWithRef(g2, "{\"type\": \"yMarker\", \"key\": \"" 1718 + m.getMarkerKey() + "\"}"); 1719 marker.draw(g2, m, false); 1720 endElement(g2); 1721 } 1722 1723 // z markers 1724 List<MarkerData> zmarkers = face.getZMarkers(); 1725 for (MarkerData m : zmarkers) { 1726 m.updateProjection(pts); 1727 beginElementWithRef(g2, "{\"type\": \"zMarker\", \"key\": \"" 1728 + m.getMarkerKey() + "\"}"); 1729 Marker marker = fetchZMarker(this.plot, m.getMarkerKey()); 1730 marker.draw(g2, m, false); 1731 endElement(g2); 1732 } 1733 } 1734 1735 /** 1736 * Returns the marker from the plot's x-axis that has the specified key, 1737 * or {@code null} if there is no marker with that key. 1738 * 1739 * @param plot the plot ({@code null} not permitted). 1740 * @param key the marker key ({@code null} not permitted). 1741 * 1742 * @return The marker (possibly {@code null}). 1743 */ 1744 private Marker fetchXMarker(Plot3D plot, String key) { 1745 if (plot instanceof CategoryPlot3D) { 1746 return ((CategoryPlot3D) plot).getColumnAxis().getMarker(key); 1747 } else if (plot instanceof XYZPlot) { 1748 return ((XYZPlot) plot).getXAxis().getMarker(key); 1749 } 1750 return null; 1751 } 1752 1753 /** 1754 * Returns the marker from the plot's y-axis that has the specified key, 1755 * or {@code null} if there is no marker with that key. 1756 * 1757 * @param plot the plot ({@code null} not permitted). 1758 * @param key the marker key ({@code null} not permitted). 1759 * 1760 * @return The marker (possibly {@code null}). 1761 */ 1762 private Marker fetchYMarker(Plot3D plot, String key) { 1763 if (plot instanceof CategoryPlot3D) { 1764 return ((CategoryPlot3D) plot).getValueAxis().getMarker(key); 1765 } else if (plot instanceof XYZPlot) { 1766 return ((XYZPlot) plot).getYAxis().getMarker(key); 1767 } 1768 return null; 1769 } 1770 1771 /** 1772 * Returns the marker from the plot's z-axis that has the specified key, 1773 * or {@code null} if there is no marker with that key. 1774 * 1775 * @param plot the plot ({@code null} not permitted). 1776 * @param key the marker key ({@code null} not permitted). 1777 * 1778 * @return The marker (possibly {@code null}). 1779 */ 1780 private Marker fetchZMarker(Plot3D plot, String key) { 1781 if (plot instanceof CategoryPlot3D) { 1782 return ((CategoryPlot3D) plot).getRowAxis().getMarker(key); 1783 } else if (plot instanceof XYZPlot) { 1784 return ((XYZPlot) plot).getZAxis().getMarker(key); 1785 } 1786 return null; 1787 } 1788 1789 /** 1790 * Receives a visitor. The visitor is first directed to the plot, then 1791 * the visit is completed for the chart. 1792 * 1793 * @param visitor the visitor. 1794 * 1795 * @since 1.2 1796 */ 1797 @Override 1798 public void receive(ChartElementVisitor visitor) { 1799 this.plot.receive(visitor); 1800 visitor.visit(this); 1801 } 1802 1803 /** 1804 * Tests this chart for equality with an arbitrary object. 1805 * 1806 * @param obj the object ({@code null} not permitted). 1807 * 1808 * @return A boolean. 1809 */ 1810 @Override 1811 public boolean equals(Object obj) { 1812 if (obj == this) { 1813 return true; 1814 } 1815 if (!(obj instanceof Chart3D)) { 1816 return false; 1817 } 1818 Chart3D that = (Chart3D) obj; 1819 if (!ObjectUtils.equals(this.background, that.background)) { 1820 return false; 1821 } 1822 if (!ObjectUtils.equals(this.title, that.title)) { 1823 return false; 1824 } 1825 if (!this.titleAnchor.equals(that.titleAnchor)) { 1826 return false; 1827 } 1828 if (!ObjectUtils.equalsPaint(this.chartBoxColor, that.chartBoxColor)) { 1829 return false; 1830 } 1831 if (!ObjectUtils.equals(this.legendBuilder, that.legendBuilder)) { 1832 return false; 1833 } 1834 if (!this.legendAnchor.equals(that.legendAnchor)) { 1835 return false; 1836 } 1837 if (this.legendOrientation != that.legendOrientation) { 1838 return false; 1839 } 1840 if (!this.renderingHints.equals(that.renderingHints)) { 1841 return false; 1842 } 1843 if (this.projDist != that.projDist) { 1844 return false; 1845 } 1846 return true; 1847 } 1848 1849 /** 1850 * A utility method that calculates a drawing area based on a bounding area 1851 * and an anchor. 1852 * 1853 * @param dim the dimensions for the drawing area ({@code null} not 1854 * permitted). 1855 * @param anchor the anchor ({@code null} not permitted). 1856 * @param bounds the bounds ({@code null} not permitted). 1857 * 1858 * @return A drawing area. 1859 */ 1860 private Rectangle2D calculateDrawArea(Dimension2D dim, Anchor2D anchor, 1861 Rectangle2D bounds) { 1862 ArgChecks.nullNotPermitted(dim, "dim"); 1863 ArgChecks.nullNotPermitted(anchor, "anchor"); 1864 ArgChecks.nullNotPermitted(bounds, "bounds"); 1865 double x, y; 1866 double w = Math.min(dim.getWidth(), bounds.getWidth()); 1867 double h = Math.min(dim.getHeight(), bounds.getHeight()); 1868 if (anchor.getRefPt().equals(RefPt2D.CENTER)) { 1869 x = bounds.getCenterX() - w / 2.0; 1870 y = bounds.getCenterY() - h / 2.0; 1871 } else if (anchor.getRefPt().equals(RefPt2D.CENTER_LEFT)) { 1872 x = bounds.getX() + anchor.getOffset().getDX(); 1873 y = bounds.getCenterY() - h / 2.0; 1874 } else if (anchor.getRefPt().equals(RefPt2D.CENTER_RIGHT)) { 1875 x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth(); 1876 y = bounds.getCenterY() - h / 2.0; 1877 } else if (anchor.getRefPt().equals(RefPt2D.TOP_CENTER)) { 1878 x = bounds.getCenterX() - w / 2.0; 1879 y = bounds.getY() + anchor.getOffset().getDY(); 1880 } else if (anchor.getRefPt().equals(RefPt2D.TOP_LEFT)) { 1881 x = bounds.getX() + anchor.getOffset().getDX(); 1882 y = bounds.getY() + anchor.getOffset().getDY(); 1883 } else if (anchor.getRefPt().equals(RefPt2D.TOP_RIGHT)) { 1884 x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth(); 1885 y = bounds.getY() + anchor.getOffset().getDY(); 1886 } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_CENTER)) { 1887 x = bounds.getCenterX() - w / 2.0; 1888 y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight(); 1889 } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_RIGHT)) { 1890 x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth(); 1891 y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight(); 1892 } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_LEFT)) { 1893 x = bounds.getX() + anchor.getOffset().getDX(); 1894 y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight(); 1895 } else { 1896 x = 0.0; 1897 y = 0.0; 1898 } 1899 return new Rectangle2D.Double(x, y, w, h); 1900 } 1901 1902 /** 1903 * Returns {@code true} if x is the longest of the four lengths, 1904 * and {@code false} otherwise. 1905 * 1906 * @param x the x-length. 1907 * @param a length 1. 1908 * @param b length 2. 1909 * @param c length 3. 1910 * 1911 * @return A boolean. 1912 */ 1913 private boolean longest(double x, double a, double b, double c) { 1914 return x >= a && x >= b && x >= c; 1915 } 1916 1917 /** 1918 * Returns the number (0, 1 or 2) arguments that have the value 1919 * {@code true}. We use this to examine the visibility of 1920 * adjacent walls of the chart box...where only one wall is visible, there 1921 * is an opportunity to display the axis along that edge. 1922 * 1923 * @param a boolean argument 1. 1924 * @param b boolean argument 2. 1925 * 1926 * @return 0, 1, or 2. 1927 */ 1928 private int count(boolean a, boolean b) { 1929 int result = 0; 1930 if (a) { 1931 result++; 1932 } 1933 if (b) { 1934 result++; 1935 } 1936 return result; 1937 } 1938 1939 /** 1940 * Receives notification of a plot change event, refreshes the 3D model 1941 * (world) and passes the event on, wrapped in a {@link Chart3DChangeEvent}, 1942 * to all registered listeners. 1943 * 1944 * @param event the plot change event. 1945 */ 1946 @Override 1947 public void plotChanged(Plot3DChangeEvent event) { 1948 if (event.requiresWorldUpdate()) { 1949 this.world = null; 1950 } 1951 notifyListeners(new Chart3DChangeEvent(event, this)); 1952 } 1953 1954 @Override 1955 public void styleChanged(ChartStyleChangeEvent event) { 1956 ChartStyler styler = new ChartStyler(event.getChartStyle()); 1957 receive(styler); 1958 // create a visitor that will visit all chart components and apply the 1959 // style 1960 notifyListeners(new Chart3DChangeEvent(event, this)); 1961 } 1962 1963 /** 1964 * Registers a listener to receive notification of changes to the chart. 1965 * 1966 * @param listener the listener ({@code null} not permitted). 1967 */ 1968 public void addChangeListener(Chart3DChangeListener listener) { 1969 this.listenerList.add(Chart3DChangeListener.class, listener); 1970 } 1971 1972 /** 1973 * Deregisters a listener so that it no longer receives notification of 1974 * changes to the chart. 1975 * 1976 * @param listener the listener ({@code null} not permitted). 1977 */ 1978 public void removeChangeListener(Chart3DChangeListener listener) { 1979 this.listenerList.remove(Chart3DChangeListener.class, listener); 1980 } 1981 1982 /** 1983 * Notifies all registered listeners that the chart has been modified. 1984 * 1985 * @param event information about the change event. 1986 */ 1987 public void notifyListeners(Chart3DChangeEvent event) { 1988 // if the 'notify' flag has been switched to false, we don't notify 1989 // the listeners 1990 if (!this.notify) { 1991 return; 1992 } 1993 Object[] listeners = this.listenerList.getListenerList(); 1994 for (int i = listeners.length - 2; i >= 0; i -= 2) { 1995 if (listeners[i] == Chart3DChangeListener.class) { 1996 ((Chart3DChangeListener) listeners[i + 1]).chartChanged(event); 1997 } 1998 } 1999 } 2000 2001 /** 2002 * Returns a flag that controls whether or not change events are sent to 2003 * registered listeners. 2004 * 2005 * @return A boolean. 2006 * 2007 * @see #setNotify(boolean) 2008 */ 2009 public boolean isNotify() { 2010 return this.notify; 2011 } 2012 2013 /** 2014 * Sets a flag that controls whether or not listeners receive 2015 * {@link Chart3DChangeEvent} notifications. 2016 * 2017 * @param notify a boolean. 2018 * 2019 * @see #isNotify() 2020 */ 2021 public void setNotify(boolean notify) { 2022 this.notify = notify; 2023 // if the flag is being set to true, there may be queued up changes... 2024 if (notify) { 2025 this.world = null; 2026 fireChangeEvent(); 2027 } 2028 } 2029 2030 /** 2031 * Sends a {@link Chart3DChangeEvent} to all registered listeners. 2032 */ 2033 protected void fireChangeEvent() { 2034 notifyListeners(new Chart3DChangeEvent(this, this)); 2035 } 2036 2037 /** 2038 * Provides serialization support. 2039 * 2040 * @param stream the input stream. 2041 * 2042 * @throws IOException if there is an I/O error. 2043 * @throws ClassNotFoundException if there is a classpath problem. 2044 */ 2045 private void readObject(ObjectInputStream stream) 2046 throws IOException, ClassNotFoundException { 2047 stream.defaultReadObject(); 2048 // recreate an empty listener list 2049 this.listenerList = new EventListenerList(); 2050 this.plot.addChangeListener(this); 2051 // RenderingHints is not easily serialized, so we just put back the 2052 // defaults... 2053 this.renderingHints = new RenderingHints( 2054 RenderingHints.KEY_ANTIALIASING, 2055 RenderingHints.VALUE_ANTIALIAS_ON); 2056 this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, 2057 RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 2058 2059 } 2060 2061 /** 2062 * Returns a string representing the {@code element}, primarily for 2063 * debugging purposes. 2064 * 2065 * @param element the element ({@code null} not permitted). 2066 * 2067 * @return A string (never {@code null}). 2068 */ 2069 public static String renderedElementToString(RenderedElement element) { 2070 Object type = element.getProperty(RenderedElement.TYPE); 2071 if (InteractiveElementType.SECTION_LABEL.equals(type)) { 2072 StringBuilder sb = new StringBuilder(); 2073 sb.append("Section label with key '"); 2074 Object key = element.getProperty("key"); 2075 sb.append(key.toString()); 2076 sb.append("'"); 2077 return sb.toString(); 2078 } else if (InteractiveElementType.LEGEND_ITEM.equals(type)) { 2079 StringBuilder sb = new StringBuilder(); 2080 sb.append("Legend item with section key '"); 2081 Object key = element.getProperty(Chart3D.SERIES_KEY); 2082 sb.append(key); 2083 sb.append("'"); 2084 return sb.toString(); 2085 } else if (InteractiveElementType.AXIS_LABEL.equals(type)) { 2086 StringBuilder sb = new StringBuilder(); 2087 sb.append("Axis label with the label '"); 2088 sb.append(element.getProperty("label")); 2089 sb.append("'"); 2090 return sb.toString(); 2091 } else if (InteractiveElementType.CATEGORY_AXIS_TICK_LABEL.equals(type)) { 2092 StringBuilder sb = new StringBuilder(); 2093 sb.append("Axis tick label with the label '"); 2094 sb.append(element.getProperty("label")); 2095 sb.append("'"); 2096 return sb.toString(); 2097 } else if (InteractiveElementType.VALUE_AXIS_TICK_LABEL.equals(type)) { 2098 StringBuilder sb = new StringBuilder(); 2099 sb.append("Axis tick label with the value '"); 2100 sb.append(element.getProperty("value")); 2101 sb.append("'"); 2102 return sb.toString(); 2103 } else if ("obj3d".equals(type)) { 2104 StringBuilder sb = new StringBuilder(); 2105 sb.append("An object in the 3D model"); 2106 ItemKey itemKey = (ItemKey) element.getProperty(Object3D.ITEM_KEY); 2107 if (itemKey != null) { 2108 sb.append(" representing the data item ["); 2109 sb.append(itemKey.toString()); 2110 sb.append("]"); 2111 } 2112 return sb.toString(); 2113 } else { 2114 return element.toString(); 2115 } 2116 } 2117 2118}