001/* =========================================================== 002 * Orson Charts : a 3D chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C)opyright 2013-2022, by David Gilbert. All rights reserved. 006 * 007 * https://github.com/jfree/orson-charts 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * Orson Charts home page: 028 * 029 * http://www.object-refinery.com/orsoncharts/index.html 030 * 031 */ 032 033package org.jfree.chart3d; 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 org.jfree.chart3d.graphics3d.internal.FaceSorter; 061import org.jfree.chart3d.graphics3d.internal.StandardFaceSorter; 062import org.jfree.chart3d.graphics3d.internal.Utils2D; 063import org.jfree.chart3d.internal.ChartBox3D; 064import org.jfree.chart3d.internal.ChartBox3D.ChartBoxFace; 065import org.jfree.chart3d.internal.OnDrawHandler; 066import org.jfree.chart3d.internal.Args; 067import org.jfree.chart3d.internal.ObjectUtils; 068import org.jfree.chart3d.internal.TextUtils; 069import org.jfree.chart3d.axis.Axis3D; 070import org.jfree.chart3d.axis.TickData; 071import org.jfree.chart3d.axis.ValueAxis3D; 072import org.jfree.chart3d.data.ItemKey; 073import org.jfree.chart3d.graphics2d.Anchor2D; 074import org.jfree.chart3d.graphics2d.RefPt2D; 075import org.jfree.chart3d.graphics2d.TextAnchor; 076import org.jfree.chart3d.graphics3d.Dimension3D; 077import org.jfree.chart3d.graphics3d.DoubleSidedFace; 078import org.jfree.chart3d.graphics3d.Drawable3D; 079import org.jfree.chart3d.graphics3d.Face; 080import org.jfree.chart3d.graphics3d.LabelFace; 081import org.jfree.chart3d.graphics3d.Object3D; 082import org.jfree.chart3d.graphics3d.Offset2D; 083import org.jfree.chart3d.graphics3d.Point3D; 084import org.jfree.chart3d.graphics3d.RenderedElement; 085import org.jfree.chart3d.graphics3d.RenderingInfo; 086import org.jfree.chart3d.graphics3d.ViewPoint3D; 087import org.jfree.chart3d.graphics3d.World; 088import org.jfree.chart3d.interaction.InteractiveElementType; 089import org.jfree.chart3d.legend.LegendAnchor; 090import org.jfree.chart3d.legend.LegendBuilder; 091import org.jfree.chart3d.legend.StandardLegendBuilder; 092import org.jfree.chart3d.marker.Marker; 093import org.jfree.chart3d.marker.MarkerData; 094import org.jfree.chart3d.plot.CategoryPlot3D; 095import org.jfree.chart3d.plot.PiePlot3D; 096import org.jfree.chart3d.plot.Plot3D; 097import org.jfree.chart3d.plot.Plot3DChangeEvent; 098import org.jfree.chart3d.plot.Plot3DChangeListener; 099import org.jfree.chart3d.plot.XYZPlot; 100import org.jfree.chart3d.style.ChartStyle; 101import org.jfree.chart3d.style.ChartStyleChangeEvent; 102import org.jfree.chart3d.style.ChartStyleChangeListener; 103import org.jfree.chart3d.style.ChartStyler; 104import org.jfree.chart3d.table.GradientRectanglePainter; 105import org.jfree.chart3d.table.RectanglePainter; 106import org.jfree.chart3d.table.StandardRectanglePainter; 107import org.jfree.chart3d.table.TableElement; 108import org.jfree.chart3d.table.TextElement; 109 110/** 111 * A chart object for 3D charts (this is the umbrella object that manages all 112 * the components of the chart). The {@link Chart3DFactory} class provides 113 * some factory methods to construct common types of charts. 114 * <br><br> 115 * All rendering is done via the Java2D API, so this object is able to draw to 116 * any implementation of the Graphics2D API (including 117 * <a href="http://www.jfree.org/jfreesvg/" target="JFreeSVG">JFreeSVG</a> for 118 * SVG output, and 119 * <a href="http://www.object-refinery.com/pdf/" target="OrsonPDF">OrsonPDF</a> 120 * for PDF output). 121 * <br><br> 122 * In the step prior to rendering, a chart is composed in a 3D model that is 123 * referred to as the "world". The dimensions of this 3D model are measured 124 * in "world units" and the overall size is controlled by the plot. You will 125 * see some attributes in the API that are specified in "world units", and these 126 * can be used to modify how objects are composed within the 3D world model. 127 * Once the objects (for example, bars in a bar chart) within the world have 128 * been composed, they are projected onto a 2D plane and rendered to the 129 * {@code Graphics2D} target (such as the screen, image, SVG file or 130 * PDF file). 131 * <br><br> 132 * Charts can have simple titles or composite titles (anything that can be 133 * constructed as a {@link TableElement} instance. The {@link TitleUtils} 134 * class contains methods to create a common title/subtitle composite title. 135 * This is illustrated in some of the demo applications. The chart title 136 * and legend (and also the axis labels) are not part of the 3D world model, 137 * they are overlaid on the output after the 3D components have been 138 * rendered. 139 * <br><br> 140 * NOTE: This class is serializable, but the serialization format is subject 141 * to change in future releases and should not be relied upon for persisting 142 * instances of this class. 143 * 144 * @see Chart3DFactory 145 * @see Chart3DPanel 146 */ 147@SuppressWarnings("serial") 148public class Chart3D implements Drawable3D, ChartElement, 149 Plot3DChangeListener, ChartStyleChangeListener, Serializable { 150 151 /** 152 * The default projection distance. 153 * 154 * @since 1.2 155 */ 156 public static final double DEFAULT_PROJ_DIST = 1500.0; 157 158 /** 159 * The key for a property that stores the interactive element type. 160 * 161 * @since 1.3 162 */ 163 public static final String INTERACTIVE_ELEMENT_TYPE 164 = "interactive_element_type"; 165 166 /** 167 * The key for a property that stores the series key. This is used to 168 * store the series key on the {@link TableElement} representing a legend 169 * item, and also on a corresponding {@link RenderedElement} after 170 * chart rendering (in the {@link RenderingInfo}). 171 * 172 * @since 1.3 173 */ 174 public static final String SERIES_KEY = "series_key"; 175 176 /** The chart id. */ 177 private String id; 178 179 /** A background rectangle painter, if any. */ 180 private RectanglePainter background; 181 182 /** The chart title (can be {@code null}). */ 183 private TableElement title; 184 185 /** The anchor point for the title (never {@code null}). */ 186 private Anchor2D titleAnchor; 187 188 /** A builder for the chart legend (can be {@code null}). */ 189 private LegendBuilder legendBuilder; 190 191 /** The anchor point for the legend (never {@code null}). */ 192 private Anchor2D legendAnchor; 193 194 /** The orientation for the legend (never {@code null}). */ 195 private Orientation legendOrientation; 196 197 /** The plot. */ 198 private Plot3D plot; 199 200 /** The view point. */ 201 private ViewPoint3D viewPoint; 202 203 /** The projection distance. */ 204 private double projDist; 205 206 /** The chart box color (never {@code null}). */ 207 private Color chartBoxColor; 208 209 /** 210 * A translation factor applied to the chart when drawing. We use this 211 * to allow the user (optionally) to drag the chart from its center 212 * location to better align it with the chart title and legend. 213 */ 214 private Offset2D translate2D; 215 216 /** Storage for registered change listeners. */ 217 private transient EventListenerList listenerList; 218 219 /** 220 * A flag that controls whether or not the chart will notify listeners 221 * of changes (defaults to {@code true}, but sometimes it is useful 222 * to disable this). 223 */ 224 private boolean notify; 225 226 /** 227 * Rendering hints that will be used for chart drawing. This can be 228 * empty but it should never be {@code null}. 229 * 230 * @since 1.1 231 */ 232 private transient RenderingHints renderingHints; 233 234 /** 235 * The chart style. 236 * 237 * @since 1.2 238 */ 239 private ChartStyle style; 240 241 /** A 3D model of the world (represents the chart). */ 242 private transient World world; 243 244 /** An object that sorts faces for rendering (painter's algorithm). */ 245 private FaceSorter faceSorter; 246 247 /** 248 * A flag that controls whether or not element hints are added to the 249 * {@code Graphics2D} output. 250 */ 251 private boolean elementHinting; 252 253 /** 254 * Creates a 3D chart for the specified plot using the default chart 255 * style. Note that a plot instance must be used in one chart instance 256 * only. 257 * 258 * @param title the chart title ({@code null} permitted). 259 * @param subtitle the chart subtitle ({@code null} permitted). 260 * @param plot the plot ({@code null} not permitted). 261 * 262 * @see Chart3DFactory 263 */ 264 public Chart3D(String title, String subtitle, Plot3D plot) { 265 this(title, subtitle, plot, Chart3DFactory.getDefaultChartStyle()); 266 } 267 268 /** 269 * Creates a 3D chart for the specified plot using the supplied style. 270 * 271 * @param title the chart title ({@code null} permitted). 272 * @param subtitle the chart subtitle ({@code null} permitted). 273 * @param plot the plot ({@code null} not permitted). 274 * @param style the chart style ({@code null} not permitted). 275 * 276 * @since 1.2 277 */ 278 public Chart3D(String title, String subtitle, Plot3D plot, 279 ChartStyle style) { 280 Args.nullNotPermitted(plot, "plot"); 281 Args.nullNotPermitted(style, "style"); 282 plot.setChart(this); 283 this.background = new StandardRectanglePainter(Color.WHITE); 284 if (title != null) { 285 this.title = TitleUtils.createTitle(title, subtitle); 286 } 287 this.titleAnchor = TitleAnchor.TOP_LEFT; 288 this.legendBuilder = new StandardLegendBuilder(); 289 this.legendAnchor = LegendAnchor.BOTTOM_RIGHT; 290 this.legendOrientation = Orientation.HORIZONTAL; 291 this.plot = plot; 292 this.plot.addChangeListener(this); 293 Dimension3D dim = this.plot.getDimensions(); 294 float distance = (float) dim.getDiagonalLength() * 3.0f; 295 this.viewPoint = ViewPoint3D.createAboveViewPoint(distance); 296 this.projDist = DEFAULT_PROJ_DIST; 297 this.chartBoxColor = new Color(255, 255, 255, 100); 298 this.translate2D = new Offset2D(); 299 this.faceSorter = new StandardFaceSorter(); 300 this.renderingHints = new RenderingHints( 301 RenderingHints.KEY_ANTIALIASING, 302 RenderingHints.VALUE_ANTIALIAS_ON); 303 this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, 304 RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 305 this.elementHinting = false; 306 this.notify = true; 307 this.listenerList = new EventListenerList(); 308 this.style = style; 309 this.style.addChangeListener(this); 310 receive(new ChartStyler(this.style)); 311 } 312 313 /** 314 * Returns the chart id. 315 * 316 * @return The chart id (possibly {@code null}). 317 * 318 * @since 1.3 319 */ 320 public String getID() { 321 return this.id; 322 } 323 324 /** 325 * Sets the chart id. 326 * 327 * @param id the id ({@code null} permitted). 328 * 329 * @since 1.3 330 */ 331 public void setID(String id) { 332 this.id = id; 333 } 334 335 /** 336 * Returns the background painter (an object that is responsible for filling 337 * the background area before charts are rendered). The default value 338 * is an instance of {@link StandardRectanglePainter} that paints the 339 * background white. 340 * 341 * @return The background painter (possibly {@code null}). 342 * 343 * @see #setBackground(org.jfree.chart3d.table.RectanglePainter) 344 */ 345 public RectanglePainter getBackground() { 346 return this.background; 347 } 348 349 /** 350 * Sets the background painter and sends a {@link Chart3DChangeEvent} to 351 * all registered listeners. A background painter is used to fill in the 352 * background of the chart before the 3D rendering takes place. To fill 353 * the background with a color or image, you can use 354 * {@link StandardRectanglePainter}. To fill the background with a 355 * gradient paint, use {@link GradientRectanglePainter}. 356 * 357 * @param background the background painter ({@code null} permitted). 358 * 359 * @see #getBackground() 360 */ 361 public void setBackground(RectanglePainter background) { 362 this.background = background; 363 fireChangeEvent(); 364 } 365 366 /** 367 * Returns the chart title. A {@link TableElement} is used for the title, 368 * since it allows a lot of flexibility in the types of title that can 369 * be displayed. 370 * 371 * @return The chart title (possibly {@code null}). 372 */ 373 public TableElement getTitle() { 374 return this.title; 375 } 376 377 /** 378 * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 379 * registered listeners. This is a convenience method that constructs 380 * the required {@link TableElement} under-the-hood. 381 * 382 * @param title the title ({@code null} permitted). 383 */ 384 public void setTitle(String title) { 385 if (title == null) { 386 setTitle((TableElement) null); 387 } else { 388 setTitle(title, this.style.getTitleFont(), 389 TitleUtils.DEFAULT_TITLE_COLOR); 390 } 391 } 392 393 /** 394 * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 395 * registered listeners. This is a convenience method that constructs 396 * the required {@link TableElement} under-the-hood. 397 * 398 * @param title the title ({@code null} not permitted). 399 * @param font the font ({@code null} not permitted). 400 * @param color the foreground color ({@code null} not permitted). 401 */ 402 public void setTitle(String title, Font font, Color color) { 403 // defer 'title' null check 404 Args.nullNotPermitted(font, "font"); 405 Args.nullNotPermitted(color, "color"); 406 TextElement te = new TextElement(title); 407 te.setTag("CHART_TITLE"); 408 te.setFont(font); 409 te.setColor(color); 410 setTitle(te); 411 } 412 413 /** 414 * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 415 * registered listeners. You can set the title to {@code null}, in 416 * which case there will be no chart title. 417 * 418 * @param title the title ({@code null} permitted). 419 */ 420 public void setTitle(TableElement title) { 421 this.title = title; 422 fireChangeEvent(); 423 } 424 425 /** 426 * Returns the title anchor. This controls the position of the title 427 * in the chart area. 428 * 429 * @return The title anchor (never {@code null}). 430 * 431 * @see #setTitleAnchor(Anchor2D) 432 */ 433 public Anchor2D getTitleAnchor() { 434 return this.titleAnchor; 435 } 436 437 /** 438 * Sets the title anchor and sends a {@link Chart3DChangeEvent} to all 439 * registered listeners. There is a {@link TitleAnchor} class providing 440 * some useful default anchors. 441 * 442 * @param anchor the anchor ({@code null} not permitted). 443 * 444 * @see #getTitleAnchor() 445 */ 446 public void setTitleAnchor(Anchor2D anchor) { 447 Args.nullNotPermitted(anchor, "anchor"); 448 this.titleAnchor = anchor; 449 fireChangeEvent(); 450 } 451 452 /** 453 * Returns the plot, which manages the dataset, the axes (if any), the 454 * renderer (if any) and other attributes related to plotting data. The 455 * plot is specified via the constructor...there is no method to set a 456 * new plot for the chart, instead you need to create a new chart instance. 457 * 458 * @return The plot (never {@code null}). 459 */ 460 public Plot3D getPlot() { 461 return this.plot; 462 } 463 464 /** 465 * Returns the chart box color (the chart box is the visible, open-sided 466 * box inside which data is plotted for all charts except pie charts). 467 * The default value is {@code Color.WHITE}. 468 * 469 * @return The chart box color (never {@code null}). 470 * 471 * @see #setChartBoxColor(java.awt.Color) 472 */ 473 public Color getChartBoxColor() { 474 return this.chartBoxColor; 475 } 476 477 /** 478 * Sets the chart box color and sends a {@link Chart3DChangeEvent} to all 479 * registered listeners. Bear in mind that {@link PiePlot3D} does not 480 * display a chart box, so this attribute will be ignored for pie charts. 481 * 482 * @param color the color ({@code null} not permitted). 483 * 484 * @see #getChartBoxColor() 485 */ 486 public void setChartBoxColor(Color color) { 487 Args.nullNotPermitted(color, "color"); 488 this.chartBoxColor = color; 489 fireChangeEvent(); 490 } 491 492 /** 493 * Returns the dimensions of the 3D object. 494 * 495 * @return The dimensions (never {@code null}). 496 */ 497 @Override 498 public Dimension3D getDimensions() { 499 return this.plot.getDimensions(); 500 } 501 502 /** 503 * Returns the view point. 504 * 505 * @return The view point (never {@code null}). 506 */ 507 @Override 508 public ViewPoint3D getViewPoint() { 509 return this.viewPoint; 510 } 511 512 /** 513 * Sets the view point. 514 * 515 * @param viewPoint the view point ({@code null} not permitted). 516 */ 517 @Override 518 public void setViewPoint(ViewPoint3D viewPoint) { 519 Args.nullNotPermitted(viewPoint, "viewPoint"); 520 this.viewPoint = viewPoint; 521 fireChangeEvent(); 522 } 523 524 /** 525 * Returns the projection distance. The default value is 526 * {@link #DEFAULT_PROJ_DIST}, higher numbers flatten out the perspective 527 * and reduce distortion in the projected image. 528 * 529 * @return The projection distance. 530 * 531 * @since 1.2 532 */ 533 @Override 534 public double getProjDistance() { 535 return this.projDist; 536 } 537 538 /** 539 * Sets the projection distance and sends a change event to all registered 540 * listeners. 541 * 542 * @param dist the distance. 543 * 544 * @since 1.2 545 */ 546 @Override 547 public void setProjDistance(double dist) { 548 this.projDist = dist; 549 fireChangeEvent(); 550 } 551 552 /** 553 * Sets the offset in 2D-space for the rendering of the chart. The 554 * default value is {@code (0, 0)} but the user can modify it via 555 * ALT-mouse-drag in the chart panel, providing an easy way to get improved 556 * chart alignment in the panels (especially prior to export to PNG, SVG or 557 * PDF). 558 * 559 * @return The offset (never {@code null}). 560 */ 561 @Override 562 public Offset2D getTranslate2D() { 563 return this.translate2D; 564 } 565 566 /** 567 * Sets the offset in 2D-space for the rendering of the chart and sends a 568 * change event to all registered listeners. 569 * 570 * @param offset the new offset ({@code null} not permitted). 571 */ 572 @Override 573 public void setTranslate2D(Offset2D offset) { 574 Args.nullNotPermitted(offset, "offset"); 575 this.translate2D = offset; 576 fireChangeEvent(); 577 } 578 579 /** 580 * Returns the legend builder. The default value is an instance of 581 * {@link StandardLegendBuilder}. If the legend builder is {@code null}, 582 * no legend will be displayed for the chart. 583 * 584 * @return The legend builder (possibly {@code null}). 585 * 586 * @see #setLegendBuilder(org.jfree.chart3d.legend.LegendBuilder) 587 * @see #setLegendAnchor(Anchor2D) 588 */ 589 public LegendBuilder getLegendBuilder() { 590 return this.legendBuilder; 591 } 592 593 /** 594 * Sets the legend builder and sends a change event to all registered 595 * listeners. When the legend builder is {@code null}, no legend 596 * will be displayed on the chart. 597 * 598 * @param legendBuilder the legend builder ({@code null} permitted). 599 * 600 * @see #setLegendAnchor(Anchor2D) 601 */ 602 public void setLegendBuilder(LegendBuilder legendBuilder) { 603 this.legendBuilder = legendBuilder; 604 fireChangeEvent(); 605 } 606 607 /** 608 * Returns the legend anchor. 609 * 610 * @return The legend anchor (never {@code null}). 611 * 612 * @see #setLegendAnchor(Anchor2D) 613 */ 614 public Anchor2D getLegendAnchor() { 615 return this.legendAnchor; 616 } 617 618 /** 619 * Sets the legend anchor and sends a {@link Chart3DChangeEvent} to all 620 * registered listeners. There is a {@link LegendAnchor} class providing 621 * some useful default anchors. 622 * 623 * @param anchor the anchor ({@code null} not permitted). 624 * 625 * @see #getLegendAnchor() 626 */ 627 public void setLegendAnchor(Anchor2D anchor) { 628 Args.nullNotPermitted(anchor, "anchor"); 629 this.legendAnchor = anchor; 630 fireChangeEvent(); 631 } 632 633 /** 634 * Returns the orientation for the legend. 635 * 636 * @return The orientation (never {@code null}). 637 * 638 * @since 1.1 639 */ 640 public Orientation getLegendOrientation() { 641 return this.legendOrientation; 642 } 643 644 /** 645 * Sets the legend orientation and sends a {@link Chart3DChangeEvent} 646 * to all registered listeners. 647 * 648 * @param orientation the orientation ({@code null} not permitted). 649 * 650 * @since 1.1 651 */ 652 public void setLegendOrientation(Orientation orientation) { 653 Args.nullNotPermitted(orientation, "orientation"); 654 this.legendOrientation = orientation; 655 fireChangeEvent(); 656 } 657 658 /** 659 * Sets the legend position (both the anchor point and the orientation) and 660 * sends a {@link Chart3DChangeEvent} to all registered listeners. 661 * This is a convenience method that calls both the 662 * {@link #setLegendAnchor(Anchor2D)} and 663 * {@link #setLegendOrientation(Orientation)} 664 * methods. 665 * 666 * @param anchor the anchor ({@code null} not permitted). 667 * @param orientation the orientation ({@code null} not permitted). 668 * 669 * @since 1.1 670 */ 671 public void setLegendPosition(Anchor2D anchor, Orientation orientation) { 672 setNotify(false); 673 setLegendAnchor(anchor); 674 setLegendOrientation(orientation); 675 setNotify(true); 676 } 677 678 /** 679 * Returns the collection of rendering hints for the chart. 680 * 681 * @return The rendering hints for the chart (never {@code null}). 682 * 683 * @see #setRenderingHints(RenderingHints) 684 * 685 * @since 1.1 686 */ 687 public RenderingHints getRenderingHints() { 688 return this.renderingHints; 689 } 690 691 /** 692 * Sets the rendering hints for the chart. These will be added (using the 693 * {@code Graphics2D.addRenderingHints()} method) near the start of 694 * the chart rendering. Note that calling this method will replace all 695 * existing hints assigned to the chart. If you simply wish to add an 696 * additional hint, you can use {@code getRenderingHints().put(key, value)}. 697 * 698 * @param hints the rendering hints ({@code null} not permitted). 699 * 700 * @see #getRenderingHints() 701 * 702 * @since 1.1 703 */ 704 public void setRenderingHints(RenderingHints hints) { 705 Args.nullNotPermitted(hints, "hints"); 706 this.renderingHints = hints; 707 fireChangeEvent(); 708 } 709 710 /** 711 * Returns a flag that indicates whether or not anti-aliasing is used when 712 * the chart is drawn. 713 * 714 * @return The flag. 715 * 716 * @see #setAntiAlias(boolean) 717 * @since 1.1 718 */ 719 public boolean getAntiAlias() { 720 Object val = this.renderingHints.get(RenderingHints.KEY_ANTIALIASING); 721 return RenderingHints.VALUE_ANTIALIAS_ON.equals(val); 722 } 723 724 /** 725 * Sets a flag that indicates whether or not anti-aliasing is used when the 726 * chart is drawn. 727 * <P> 728 * Anti-aliasing usually improves the appearance of charts, but is slower. 729 * 730 * @param flag the new value of the flag. 731 * 732 * @see #getAntiAlias() 733 * @since 1.1 734 */ 735 public void setAntiAlias(boolean flag) { 736 if (flag) { 737 this.renderingHints.put(RenderingHints.KEY_ANTIALIASING, 738 RenderingHints.VALUE_ANTIALIAS_ON); 739 } else { 740 this.renderingHints.put(RenderingHints.KEY_ANTIALIASING, 741 RenderingHints.VALUE_ANTIALIAS_OFF); 742 } 743 fireChangeEvent(); 744 } 745 746 /** 747 * Returns the flag that controls whether or not element hints will be 748 * added to the {@code Graphics2D} output when the chart is rendered. 749 * The default value is {@code false}. 750 * 751 * @return A boolean. 752 * 753 * @since 1.3 754 */ 755 public boolean getElementHinting() { 756 return this.elementHinting; 757 } 758 759 /** 760 * Sets the flag that controls whether or not element hints will be 761 * added to the {@code Graphics2D} output when the chart is rendered 762 * and sends a change event to all registered listeners. 763 * 764 * @param hinting the new flag value. 765 * 766 * @since 1.3 767 */ 768 public void setElementHinting(boolean hinting) { 769 this.elementHinting = hinting; 770 fireChangeEvent(); 771 } 772 773 /** 774 * Returns the chart style. 775 * 776 * @return The chart style (never {@code null}). 777 * 778 * @since 1.2 779 */ 780 public ChartStyle getStyle() { 781 return this.style; 782 } 783 784 /** 785 * Sets (and applies) the specified chart style. 786 * 787 * @param style the chart style ({@code null} not permitted). 788 * 789 * @since 1.2 790 */ 791 public void setStyle(ChartStyle style) { 792 Args.nullNotPermitted(style, "style"); 793 this.style.removeChangeListener(this); 794 this.style = style; 795 this.style.addChangeListener(this); 796 setNotify(false); 797 receive(new ChartStyler(this.style)); 798 setNotify(true); 799 } 800 801 /** 802 * Creates a world containing the chart and the supplied chart box. 803 * 804 * @param chartBox the chart box ({@code null} permitted). 805 */ 806 private World createWorld(ChartBox3D chartBox) { 807 World result = new World(); 808 Dimension3D dim = this.plot.getDimensions(); 809 double w = dim.getWidth(); 810 double h = dim.getHeight(); 811 double d = dim.getDepth(); 812 if (chartBox != null) { 813 result.add("chartbox", chartBox.createObject3D()); 814 } 815 this.plot.compose(result, -w / 2, -h / 2, -d / 2); 816 return result; 817 } 818 819 /** 820 * Draws the chart to the specified output target. 821 * 822 * @param g2 the output target ({@code null} not permitted). 823 * 824 * @return Information about the items rendered. 825 */ 826 @Override 827 public RenderingInfo draw(Graphics2D g2, Rectangle2D bounds) { 828 beginElement(g2, this.id, "ORSON_CHART_TOP_LEVEL"); 829 Shape savedClip = g2.getClip(); 830 g2.clip(bounds); 831 g2.addRenderingHints(this.renderingHints); 832 g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, 833 BasicStroke.JOIN_ROUND, 1f)); 834 Dimension3D dim3D = this.plot.getDimensions(); 835 double w = dim3D.getWidth(); 836 double h = dim3D.getHeight(); 837 double depth = dim3D.getDepth(); 838 ChartBox3D chartBox = null; 839 if (this.plot instanceof XYZPlot 840 || this.plot instanceof CategoryPlot3D) { 841 double[] tickUnits = findAxisTickUnits(g2, w, h, depth); 842 chartBox = new ChartBox3D(w, h, depth, -w / 2, -h / 2, -depth / 2, 843 this.chartBoxColor); 844 chartBox.setXTicks(fetchXTickData(this.plot, tickUnits[0])); 845 chartBox.setYTicks(fetchYTickData(this.plot, tickUnits[1])); 846 chartBox.setZTicks(fetchZTickData(this.plot, tickUnits[2])); 847 chartBox.setXMarkers(fetchXMarkerData(this.plot)); 848 chartBox.setYMarkers(fetchYMarkerData(this.plot)); 849 chartBox.setZMarkers(fetchZMarkerData(this.plot)); 850 } 851 if (this.world == null) { 852 this.world = createWorld(chartBox); 853 } else if (chartBox != null) { 854 this.world.clear("chartbox"); 855 this.world.add("chartbox", chartBox.createObject3D()); 856 } 857 if (this.background != null) { 858 this.background.fill(g2, bounds); 859 } 860 AffineTransform saved = g2.getTransform(); 861 double dx = bounds.getX() + bounds.getWidth() / 2.0 862 + this.translate2D.getDX(); 863 double dy = bounds.getY() + bounds.getHeight() / 2.0 864 + this.translate2D.getDY(); 865 g2.translate(dx, dy); 866 Point3D[] eyePts = this.world.calculateEyeCoordinates(this.viewPoint); 867 Point2D[] pts = this.world.calculateProjectedPoints(this.viewPoint, 868 this.projDist); 869 870 // sort faces by z-order 871 List<Face> facesInPaintOrder = new ArrayList<>(world.getFaces()); 872 facesInPaintOrder = this.faceSorter.sort(facesInPaintOrder, eyePts); 873 Line2D line = null; 874 Stroke stroke = new BasicStroke(1.0f); 875 for (Face f : facesInPaintOrder) { 876 // check for the special case where the face is just a line 877 if (f.getVertexCount() == 2) { 878 g2.setPaint(f.getColor()); 879 if (line == null) { 880 line = new Line2D.Float(); 881 } 882 int v0 = f.getVertexIndex(0); 883 int v1 = f.getVertexIndex(1); 884 line.setLine(pts[v0].getX(), pts[v0].getY(), pts[v1].getX(), 885 pts[v1].getY()); 886 g2.setStroke(stroke); 887 g2.draw(line); 888 continue; 889 } 890 boolean drawOutline = f.getOutline(); 891 double[] plane = f.calculateNormal(eyePts); 892 double inprod = plane[0] * world.getSunX() + plane[1] 893 * world.getSunY() + plane[2] * world.getSunZ(); 894 double shade = (inprod + 1) / 2.0; 895 if (f instanceof DoubleSidedFace 896 || Utils2D.area2(pts[f.getVertexIndex(0)], 897 pts[f.getVertexIndex(1)], pts[f.getVertexIndex(2)]) > 0.0) { 898 Color c = f.getColor(); 899 Path2D p = f.createPath(pts); 900 g2.setPaint(new Color((int) (c.getRed() * shade), 901 (int) (c.getGreen() * shade), 902 (int) (c.getBlue() * shade), c.getAlpha())); 903 if (this.elementHinting) { 904 beginElementGroup(f, g2); 905 } 906 g2.fill(p); 907 if (drawOutline) { 908 g2.draw(p); 909 } 910 if (this.elementHinting) { 911 endElementGroup(f, g2); 912 } 913 914 if (f instanceof ChartBoxFace 915 && (this.plot instanceof CategoryPlot3D 916 || this.plot instanceof XYZPlot)) { 917 Stroke savedStroke = g2.getStroke(); 918 ChartBoxFace cbf = (ChartBoxFace) f; 919 drawGridlines(g2, cbf, pts); 920 drawMarkers(g2, cbf, pts); 921 g2.setStroke(savedStroke); 922 } 923 } else if (f instanceof LabelFace) { 924 LabelFace lf = (LabelFace) f; 925 Path2D p = lf.createPath(pts); 926 Rectangle2D lb = p.getBounds2D(); 927 g2.setFont(lf.getFont()); 928 g2.setColor(lf.getBackgroundColor()); 929 Rectangle2D bb = TextUtils.calcAlignedStringBounds( 930 lf.getLabel(), g2, 931 (float) lb.getCenterX(), (float) lb.getCenterY(), 932 TextAnchor.CENTER); 933 g2.fill(bb); 934 g2.setColor(lf.getTextColor()); 935 Rectangle2D r = TextUtils.drawAlignedString(lf.getLabel(), g2, 936 (float) lb.getCenterX(), (float) lb.getCenterY(), 937 TextAnchor.CENTER); 938 lf.getOwner().setProperty("labelBounds", r); 939 } 940 } 941 RenderingInfo info = new RenderingInfo(facesInPaintOrder, pts, dx, dy); 942 OnDrawHandler onDrawHandler = new OnDrawHandler(info, 943 this.elementHinting); 944 945 // handle labels on pie plots... 946 if (this.plot instanceof PiePlot3D) { 947 drawPieLabels(g2, w, h, depth, info); 948 } 949 950 // handle axis labelling on non-pie plots... 951 if (this.plot instanceof XYZPlot || this.plot instanceof 952 CategoryPlot3D) { 953 drawAxes(g2, chartBox, pts, info); 954 } 955 956 g2.setTransform(saved); 957 958 // generate and draw the legend... 959 if (this.legendBuilder != null) { 960 TableElement legend = this.legendBuilder.createLegend(this.plot, 961 this.legendAnchor, this.legendOrientation, this.style); 962 if (legend != null) { 963 Dimension2D legendSize = legend.preferredSize(g2, bounds); 964 Rectangle2D legendArea = calculateDrawArea(legendSize, 965 this.legendAnchor, bounds); 966 legend.draw(g2, legendArea, onDrawHandler); 967 } 968 } 969 970 // draw the title... 971 if (this.title != null) { 972 Dimension2D titleSize = this.title.preferredSize(g2, bounds); 973 Rectangle2D titleArea = calculateDrawArea(titleSize, 974 this.titleAnchor, bounds); 975 this.title.draw(g2, titleArea, onDrawHandler); 976 } 977 g2.setClip(savedClip); 978 endElement(g2); 979 return info; 980 } 981 982 private void beginElementGroup(Face face, Graphics2D g2) { 983 Object3D owner = face.getOwner(); 984 ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY); 985 if (itemKey != null) { 986 Map<String, String> m = new HashMap<>(); 987 m.put("ref", itemKey.toJSONString()); 988 g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); 989 } 990 } 991 992 private void endElementGroup(Face face, Graphics2D g2) { 993 Object3D owner = face.getOwner(); 994 ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY); 995 if (itemKey != null) { 996 g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE); 997 } 998 } 999 1000 /** 1001 * An implementation method that fetches x-axis tick data from the plot, 1002 * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}. 1003 * On a category plot, the x-axis is the column axis (and the tickUnit is 1004 * ignored). 1005 * 1006 * @param plot the plot. 1007 * @param tickUnit the tick unit. 1008 * 1009 * @return A list of tick data instances representing the tick marks and 1010 * values along the x-axis. 1011 */ 1012 private List<TickData> fetchXTickData(Plot3D plot, double tickUnit) { 1013 if (plot instanceof CategoryPlot3D) { 1014 CategoryPlot3D cp = (CategoryPlot3D) plot; 1015 return cp.getColumnAxis().generateTickDataForColumns( 1016 cp.getDataset()); 1017 } 1018 if (plot instanceof XYZPlot) { 1019 XYZPlot xp = (XYZPlot) plot; 1020 return xp.getXAxis().generateTickData(tickUnit); 1021 } 1022 return Collections.emptyList(); 1023 } 1024 1025 /** 1026 * An implementation method that fetches y-axis tick data from the plot, 1027 * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}. 1028 * On a category plot, the y-axis is the value axis. 1029 * 1030 * @param plot the plot. 1031 * @param tickUnit the tick unit. 1032 * 1033 * @return A list of tick data instances representing the tick marks and 1034 * values along the y-axis. 1035 */ 1036 private List<TickData> fetchYTickData(Plot3D plot, double tickUnit) { 1037 if (plot instanceof CategoryPlot3D) { 1038 CategoryPlot3D cp = (CategoryPlot3D) plot; 1039 return cp.getValueAxis().generateTickData(tickUnit); 1040 } 1041 if (plot instanceof XYZPlot) { 1042 XYZPlot xp = (XYZPlot) plot; 1043 return xp.getYAxis().generateTickData(tickUnit); 1044 } 1045 return Collections.emptyList(); 1046 } 1047 1048 /** 1049 * An implementation method that fetches z-axis tick data from the plot, 1050 * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}. 1051 * On a category plot, the z-axis is the row axis (and the tickUnit is 1052 * ignored). 1053 * 1054 * @param plot the plot. 1055 * @param tickUnit the tick unit. 1056 * 1057 * @return A list of tick data instances representing the tick marks and 1058 * values along the y-axis. 1059 */ 1060 private List<TickData> fetchZTickData(Plot3D plot, double tickUnit) { 1061 if (plot instanceof CategoryPlot3D) { 1062 CategoryPlot3D cp = (CategoryPlot3D) plot; 1063 return cp.getRowAxis().generateTickDataForRows(cp.getDataset()); 1064 } 1065 if (plot instanceof XYZPlot) { 1066 XYZPlot xp = (XYZPlot) plot; 1067 return xp.getZAxis().generateTickData(tickUnit); 1068 } 1069 return Collections.emptyList(); 1070 } 1071 1072 /** 1073 * Fetches marker data for the plot's x-axis. 1074 * 1075 * @param plot the plot ({@code null} not permitted). 1076 * 1077 * @return A list of marker data items (possibly empty but never 1078 * {@code null}). 1079 */ 1080 private List<MarkerData> fetchXMarkerData(Plot3D plot) { 1081 if (plot instanceof CategoryPlot3D) { 1082 return ((CategoryPlot3D) plot).getColumnAxis().generateMarkerData(); 1083 } 1084 if (plot instanceof XYZPlot) { 1085 return ((XYZPlot) plot).getXAxis().generateMarkerData(); 1086 } 1087 return new ArrayList<>(0); 1088 } 1089 1090 /** 1091 * Fetches marker data for the plot's x-axis. 1092 * 1093 * @param plot the plot ({@code null} not permitted). 1094 * 1095 * @return A list of marker data items (possibly empty but never 1096 * {@code null}). 1097 */ 1098 private List<MarkerData> fetchYMarkerData(Plot3D plot) { 1099 if (plot instanceof CategoryPlot3D) { 1100 return ((CategoryPlot3D) plot).getValueAxis().generateMarkerData(); 1101 } 1102 if (plot instanceof XYZPlot) { 1103 return ((XYZPlot) plot).getYAxis().generateMarkerData(); 1104 } 1105 return new ArrayList<>(0); 1106 } 1107 1108 /** 1109 * Fetches marker data for the plot's x-axis. 1110 * 1111 * @param plot the plot ({@code null} not permitted). 1112 * 1113 * @return A list of marker data items (possibly empty but never 1114 * {@code null}). 1115 */ 1116 private List<MarkerData> fetchZMarkerData(Plot3D plot) { 1117 if (plot instanceof CategoryPlot3D) { 1118 return ((CategoryPlot3D) plot).getRowAxis().generateMarkerData(); 1119 } 1120 if (plot instanceof XYZPlot) { 1121 return ((XYZPlot) plot).getZAxis().generateMarkerData(); 1122 } 1123 return new ArrayList<>(0); 1124 } 1125 1126 /** 1127 * Draw the gridlines for one chart box face. 1128 * 1129 * @param g2 the graphics target. 1130 * @param face the face. 1131 * @param pts the projection points. 1132 */ 1133 private void drawGridlines(Graphics2D g2, ChartBoxFace face, 1134 Point2D[] pts) { 1135 if (isGridlinesVisibleForX(this.plot)) { 1136 g2.setPaint(fetchGridlinePaintX(this.plot)); 1137 g2.setStroke(fetchGridlineStrokeX(this.plot)); 1138 List<TickData> xA = face.getXTicksA(); 1139 List<TickData> xB = face.getXTicksB(); 1140 for (int i = 0; i < xA.size(); i++) { 1141 Line2D line = new Line2D.Double( 1142 pts[face.getOffset() + xA.get(i).getVertexIndex()], 1143 pts[face.getOffset() + xB.get(i).getVertexIndex()]); 1144 g2.draw(line); 1145 } 1146 } 1147 1148 if (isGridlinesVisibleForY(this.plot)) { 1149 g2.setPaint(fetchGridlinePaintY(this.plot)); 1150 g2.setStroke(fetchGridlineStrokeY(this.plot)); 1151 List<TickData> yA = face.getYTicksA(); 1152 List<TickData> yB = face.getYTicksB(); 1153 for (int i = 0; i < yA.size(); i++) { 1154 Line2D line = new Line2D.Double( 1155 pts[face.getOffset() + yA.get(i).getVertexIndex()], 1156 pts[face.getOffset() + yB.get(i).getVertexIndex()]); 1157 g2.draw(line); 1158 } 1159 } 1160 1161 if (isGridlinesVisibleForZ(this.plot)) { 1162 g2.setPaint(fetchGridlinePaintZ(this.plot)); 1163 g2.setStroke(fetchGridlineStrokeZ(this.plot)); 1164 List<TickData> zA = face.getZTicksA(); 1165 List<TickData> zB = face.getZTicksB(); 1166 for (int i = 0; i < zA.size(); i++) { 1167 Line2D line = new Line2D.Double( 1168 pts[face.getOffset() + zA.get(i).getVertexIndex()], 1169 pts[face.getOffset() + zB.get(i).getVertexIndex()]); 1170 g2.draw(line); 1171 } 1172 } 1173 } 1174 1175 /** 1176 * Returns {@code true} if gridlines are visible for the x-axis 1177 * (column axis in the case of a {@link CategoryPlot3D}) and 1178 * {@code false} otherwise. For pie charts, this method will always 1179 * return {@code false}. 1180 * 1181 * @param plot the plot. 1182 * 1183 * @return A boolean. 1184 */ 1185 private boolean isGridlinesVisibleForX(Plot3D plot) { 1186 if (plot instanceof CategoryPlot3D) { 1187 CategoryPlot3D cp = (CategoryPlot3D) plot; 1188 return cp.getGridlinesVisibleForColumns(); 1189 } 1190 if (plot instanceof XYZPlot) { 1191 XYZPlot xp = (XYZPlot) plot; 1192 return xp.isGridlinesVisibleX(); 1193 } 1194 return false; 1195 } 1196 1197 /** 1198 * Returns {@code true} if gridlines are visible for the y-axis 1199 * (value axis in the case of a {@link CategoryPlot3D}) and 1200 * {@code false} otherwise. 1201 * 1202 * @param plot the plot. 1203 * 1204 * @return A boolean. 1205 */ 1206 private boolean isGridlinesVisibleForY(Plot3D plot) { 1207 if (plot instanceof CategoryPlot3D) { 1208 CategoryPlot3D cp = (CategoryPlot3D) plot; 1209 return cp.getGridlinesVisibleForValues(); 1210 } 1211 if (plot instanceof XYZPlot) { 1212 XYZPlot xp = (XYZPlot) plot; 1213 return xp.isGridlinesVisibleY(); 1214 } 1215 return false; 1216 } 1217 1218 /** 1219 * Returns {@code true} if gridlines are visible for the z-axis 1220 * (row axis in the case of a {@link CategoryPlot3D}) and 1221 * {@code false} otherwise. 1222 * 1223 * @param plot the plot. 1224 * 1225 * @return A boolean. 1226 */ 1227 private boolean isGridlinesVisibleForZ(Plot3D plot) { 1228 if (plot instanceof CategoryPlot3D) { 1229 CategoryPlot3D cp = (CategoryPlot3D) plot; 1230 return cp.getGridlinesVisibleForRows(); 1231 } 1232 if (plot instanceof XYZPlot) { 1233 XYZPlot xp = (XYZPlot) plot; 1234 return xp.isGridlinesVisibleZ(); 1235 } 1236 return false; 1237 } 1238 1239 /** 1240 * Returns the paint used to draw gridlines on the x-axis (or column axis 1241 * in the case of {@link CategoryPlot3D}). 1242 * 1243 * @param plot the plot. 1244 * 1245 * @return The paint. 1246 */ 1247 private Paint fetchGridlinePaintX(Plot3D plot) { 1248 if (plot instanceof CategoryPlot3D) { 1249 CategoryPlot3D cp = (CategoryPlot3D) plot; 1250 return cp.getGridlinePaintForColumns(); 1251 } 1252 if (plot instanceof XYZPlot) { 1253 XYZPlot xp = (XYZPlot) plot; 1254 return xp.getGridlinePaintX(); 1255 } 1256 return null; 1257 } 1258 1259 /** 1260 * Returns the paint used to draw gridlines on the y-axis (or value axis 1261 * in the case of {@link CategoryPlot3D}). 1262 * 1263 * @param plot the plot. 1264 * 1265 * @return The paint. 1266 */ 1267 private Paint fetchGridlinePaintY(Plot3D plot) { 1268 if (plot instanceof CategoryPlot3D) { 1269 CategoryPlot3D cp = (CategoryPlot3D) plot; 1270 return cp.getGridlinePaintForValues(); 1271 } 1272 if (plot instanceof XYZPlot) { 1273 XYZPlot xp = (XYZPlot) plot; 1274 return xp.getGridlinePaintY(); 1275 } 1276 return null; 1277 } 1278 1279 /** 1280 * Returns the paint used to draw gridlines on the z-axis (or row axis 1281 * in the case of {@link CategoryPlot3D}). 1282 * 1283 * @param plot the plot. 1284 * 1285 * @return The paint. 1286 */ 1287 private Paint fetchGridlinePaintZ(Plot3D plot) { 1288 if (plot instanceof CategoryPlot3D) { 1289 CategoryPlot3D cp = (CategoryPlot3D) plot; 1290 return cp.getGridlinePaintForRows(); 1291 } 1292 if (plot instanceof XYZPlot) { 1293 XYZPlot xp = (XYZPlot) plot; 1294 return xp.getGridlinePaintZ(); 1295 } 1296 return null; 1297 } 1298 1299 /** 1300 * Returns the stroke used to draw gridlines on the x-axis (or column axis 1301 * in the case of {@link CategoryPlot3D}). 1302 * 1303 * @param plot the plot. 1304 * 1305 * @return The stroke. 1306 */ 1307 private Stroke fetchGridlineStrokeX(Plot3D plot) { 1308 if (plot instanceof CategoryPlot3D) { 1309 CategoryPlot3D cp = (CategoryPlot3D) plot; 1310 return cp.getGridlineStrokeForColumns(); 1311 } 1312 if (plot instanceof XYZPlot) { 1313 XYZPlot xp = (XYZPlot) plot; 1314 return xp.getGridlineStrokeX(); 1315 } 1316 return null; 1317 } 1318 1319 /** 1320 * Returns the stroke used to draw gridlines on the y-axis (or value axis 1321 * in the case of {@link CategoryPlot3D}). 1322 * 1323 * @param plot the plot. 1324 * 1325 * @return The stroke. 1326 */ 1327 private Stroke fetchGridlineStrokeY(Plot3D plot) { 1328 if (plot instanceof CategoryPlot3D) { 1329 CategoryPlot3D cp = (CategoryPlot3D) plot; 1330 return cp.getGridlineStrokeForValues(); 1331 } 1332 if (plot instanceof XYZPlot) { 1333 XYZPlot xp = (XYZPlot) plot; 1334 return xp.getGridlineStrokeY(); 1335 } 1336 return null; 1337 } 1338 1339 /** 1340 * Returns the stroke used to draw gridlines on the z-axis (or row axis 1341 * in the case of {@link CategoryPlot3D}). 1342 * 1343 * @param plot the plot. 1344 * 1345 * @return The stroke. 1346 */ 1347 private Stroke fetchGridlineStrokeZ(Plot3D plot) { 1348 if (plot instanceof CategoryPlot3D) { 1349 CategoryPlot3D cp = (CategoryPlot3D) plot; 1350 return cp.getGridlineStrokeForRows(); 1351 } 1352 if (plot instanceof XYZPlot) { 1353 XYZPlot xp = (XYZPlot) plot; 1354 return xp.getGridlineStrokeZ(); 1355 } 1356 return null; 1357 } 1358 1359 /** 1360 * Draws the pie labels for a {@link PiePlot3D} in 2D-space by creating a 1361 * temporary world with vertices at anchor points for the labels, then 1362 * projecting the points to 2D-space. 1363 * 1364 * @param g2 the graphics target. 1365 * @param w the width. 1366 * @param h the height. 1367 * @param depth the depth. 1368 * @param info the rendering info ({@code null} permitted). 1369 */ 1370 @SuppressWarnings("unchecked") 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<String, String> m = new HashMap<>(); 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 Args.nullNotPermitted(dim, "dim"); 1863 Args.nullNotPermitted(anchor, "anchor"); 1864 Args.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 return "Axis label with the label '" + element.getProperty("label") + "'"; 2087 } else if (InteractiveElementType.CATEGORY_AXIS_TICK_LABEL.equals(type)) { 2088 return "Axis tick label with the label '" + element.getProperty("label") + "'"; 2089 } else if (InteractiveElementType.VALUE_AXIS_TICK_LABEL.equals(type)) { 2090 return "Axis tick label with the value '" + element.getProperty("value") + "'"; 2091 } else if ("obj3d".equals(type)) { 2092 StringBuilder sb = new StringBuilder(); 2093 sb.append("An object in the 3D model"); 2094 ItemKey itemKey = (ItemKey) element.getProperty(Object3D.ITEM_KEY); 2095 if (itemKey != null) { 2096 sb.append(" representing the data item ["); 2097 sb.append(itemKey); 2098 sb.append("]"); 2099 } 2100 return sb.toString(); 2101 } else { 2102 return element.toString(); 2103 } 2104 } 2105 2106}