001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * --------------------- 028 * PaintScaleLegend.java 029 * --------------------- 030 * (C) Copyright 2007-2021, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Peter Kolb - see patch 2686872; 034 * 035 */ 036 037package org.jfree.chart.title; 038 039import java.awt.BasicStroke; 040import java.awt.Color; 041import java.awt.Graphics2D; 042import java.awt.Paint; 043import java.awt.Stroke; 044import java.awt.geom.Rectangle2D; 045import java.io.IOException; 046import java.io.ObjectInputStream; 047import java.io.ObjectOutputStream; 048 049import org.jfree.chart.axis.AxisLocation; 050import org.jfree.chart.axis.AxisSpace; 051import org.jfree.chart.axis.ValueAxis; 052import org.jfree.chart.block.LengthConstraintType; 053import org.jfree.chart.block.RectangleConstraint; 054import org.jfree.chart.event.AxisChangeEvent; 055import org.jfree.chart.event.AxisChangeListener; 056import org.jfree.chart.event.TitleChangeEvent; 057import org.jfree.chart.plot.Plot; 058import org.jfree.chart.plot.PlotOrientation; 059import org.jfree.chart.renderer.PaintScale; 060import org.jfree.chart.ui.RectangleEdge; 061import org.jfree.chart.ui.Size2D; 062import org.jfree.chart.util.PaintUtils; 063import org.jfree.chart.util.Args; 064import org.jfree.chart.util.PublicCloneable; 065import org.jfree.chart.util.SerialUtils; 066import org.jfree.data.Range; 067 068/** 069 * A legend that shows a range of values and their associated colors, driven 070 * by an underlying {@link PaintScale} implementation. 071 */ 072public class PaintScaleLegend extends Title implements AxisChangeListener, 073 PublicCloneable { 074 075 /** For serialization. */ 076 static final long serialVersionUID = -1365146490993227503L; 077 078 /** The paint scale (never {@code null}). */ 079 private PaintScale scale; 080 081 /** The value axis (never {@code null}). */ 082 private ValueAxis axis; 083 084 /** 085 * The axis location (handles both orientations, never 086 * {@code null}). 087 */ 088 private AxisLocation axisLocation; 089 090 /** The offset between the axis and the paint strip (in Java2D units). */ 091 private double axisOffset; 092 093 /** The thickness of the paint strip (in Java2D units). */ 094 private double stripWidth; 095 096 /** 097 * A flag that controls whether or not an outline is drawn around the 098 * paint strip. 099 */ 100 private boolean stripOutlineVisible; 101 102 /** The paint used to draw an outline around the paint strip. */ 103 private transient Paint stripOutlinePaint; 104 105 /** The stroke used to draw an outline around the paint strip. */ 106 private transient Stroke stripOutlineStroke; 107 108 /** The background paint (never {@code null}). */ 109 private transient Paint backgroundPaint; 110 111 /** 112 * The number of subdivisions for the scale when rendering. 113 */ 114 private int subdivisions; 115 116 /** 117 * Creates a new instance. 118 * 119 * @param scale the scale ({@code null} not permitted). 120 * @param axis the axis ({@code null} not permitted). 121 */ 122 public PaintScaleLegend(PaintScale scale, ValueAxis axis) { 123 Args.nullNotPermitted(axis, "axis"); 124 this.scale = scale; 125 this.axis = axis; 126 this.axis.addChangeListener(this); 127 this.axisLocation = AxisLocation.BOTTOM_OR_LEFT; 128 this.axisOffset = 0.0; 129 this.axis.setRange(scale.getLowerBound(), scale.getUpperBound()); 130 this.stripWidth = 15.0; 131 this.stripOutlineVisible = true; 132 this.stripOutlinePaint = Color.GRAY; 133 this.stripOutlineStroke = new BasicStroke(0.5f); 134 this.backgroundPaint = Color.WHITE; 135 this.subdivisions = 100; 136 } 137 138 /** 139 * Returns the scale used to convert values to colors. 140 * 141 * @return The scale (never {@code null}). 142 * 143 * @see #setScale(PaintScale) 144 */ 145 public PaintScale getScale() { 146 return this.scale; 147 } 148 149 /** 150 * Sets the scale and sends a {@link TitleChangeEvent} to all registered 151 * listeners. 152 * 153 * @param scale the scale ({@code null} not permitted). 154 * 155 * @see #getScale() 156 */ 157 public void setScale(PaintScale scale) { 158 Args.nullNotPermitted(scale, "scale"); 159 this.scale = scale; 160 notifyListeners(new TitleChangeEvent(this)); 161 } 162 163 /** 164 * Returns the axis for the paint scale. 165 * 166 * @return The axis (never {@code null}). 167 * 168 * @see #setAxis(ValueAxis) 169 */ 170 public ValueAxis getAxis() { 171 return this.axis; 172 } 173 174 /** 175 * Sets the axis for the paint scale and sends a {@link TitleChangeEvent} 176 * to all registered listeners. 177 * 178 * @param axis the axis ({@code null} not permitted). 179 * 180 * @see #getAxis() 181 */ 182 public void setAxis(ValueAxis axis) { 183 Args.nullNotPermitted(axis, "axis"); 184 this.axis.removeChangeListener(this); 185 this.axis = axis; 186 this.axis.addChangeListener(this); 187 notifyListeners(new TitleChangeEvent(this)); 188 } 189 190 /** 191 * Returns the axis location. 192 * 193 * @return The axis location (never {@code null}). 194 * 195 * @see #setAxisLocation(AxisLocation) 196 */ 197 public AxisLocation getAxisLocation() { 198 return this.axisLocation; 199 } 200 201 /** 202 * Sets the axis location and sends a {@link TitleChangeEvent} to all 203 * registered listeners. 204 * 205 * @param location the location ({@code null} not permitted). 206 * 207 * @see #getAxisLocation() 208 */ 209 public void setAxisLocation(AxisLocation location) { 210 Args.nullNotPermitted(location, "location"); 211 this.axisLocation = location; 212 notifyListeners(new TitleChangeEvent(this)); 213 } 214 215 /** 216 * Returns the offset between the axis and the paint strip. 217 * 218 * @return The offset between the axis and the paint strip. 219 * 220 * @see #setAxisOffset(double) 221 */ 222 public double getAxisOffset() { 223 return this.axisOffset; 224 } 225 226 /** 227 * Sets the offset between the axis and the paint strip and sends a 228 * {@link TitleChangeEvent} to all registered listeners. 229 * 230 * @param offset the offset. 231 */ 232 public void setAxisOffset(double offset) { 233 this.axisOffset = offset; 234 notifyListeners(new TitleChangeEvent(this)); 235 } 236 237 /** 238 * Returns the width of the paint strip, in Java2D units. 239 * 240 * @return The width of the paint strip. 241 * 242 * @see #setStripWidth(double) 243 */ 244 public double getStripWidth() { 245 return this.stripWidth; 246 } 247 248 /** 249 * Sets the width of the paint strip and sends a {@link TitleChangeEvent} 250 * to all registered listeners. 251 * 252 * @param width the width. 253 * 254 * @see #getStripWidth() 255 */ 256 public void setStripWidth(double width) { 257 this.stripWidth = width; 258 notifyListeners(new TitleChangeEvent(this)); 259 } 260 261 /** 262 * Returns the flag that controls whether or not an outline is drawn 263 * around the paint strip. 264 * 265 * @return A boolean. 266 * 267 * @see #setStripOutlineVisible(boolean) 268 */ 269 public boolean isStripOutlineVisible() { 270 return this.stripOutlineVisible; 271 } 272 273 /** 274 * Sets the flag that controls whether or not an outline is drawn around 275 * the paint strip, and sends a {@link TitleChangeEvent} to all registered 276 * listeners. 277 * 278 * @param visible the flag. 279 * 280 * @see #isStripOutlineVisible() 281 */ 282 public void setStripOutlineVisible(boolean visible) { 283 this.stripOutlineVisible = visible; 284 notifyListeners(new TitleChangeEvent(this)); 285 } 286 287 /** 288 * Returns the paint used to draw the outline of the paint strip. 289 * 290 * @return The paint (never {@code null}). 291 * 292 * @see #setStripOutlinePaint(Paint) 293 */ 294 public Paint getStripOutlinePaint() { 295 return this.stripOutlinePaint; 296 } 297 298 /** 299 * Sets the paint used to draw the outline of the paint strip, and sends 300 * a {@link TitleChangeEvent} to all registered listeners. 301 * 302 * @param paint the paint ({@code null} not permitted). 303 * 304 * @see #getStripOutlinePaint() 305 */ 306 public void setStripOutlinePaint(Paint paint) { 307 Args.nullNotPermitted(paint, "paint"); 308 this.stripOutlinePaint = paint; 309 notifyListeners(new TitleChangeEvent(this)); 310 } 311 312 /** 313 * Returns the stroke used to draw the outline around the paint strip. 314 * 315 * @return The stroke (never {@code null}). 316 * 317 * @see #setStripOutlineStroke(Stroke) 318 */ 319 public Stroke getStripOutlineStroke() { 320 return this.stripOutlineStroke; 321 } 322 323 /** 324 * Sets the stroke used to draw the outline around the paint strip and 325 * sends a {@link TitleChangeEvent} to all registered listeners. 326 * 327 * @param stroke the stroke ({@code null} not permitted). 328 * 329 * @see #getStripOutlineStroke() 330 */ 331 public void setStripOutlineStroke(Stroke stroke) { 332 Args.nullNotPermitted(stroke, "stroke"); 333 this.stripOutlineStroke = stroke; 334 notifyListeners(new TitleChangeEvent(this)); 335 } 336 337 /** 338 * Returns the background paint. 339 * 340 * @return The background paint. 341 */ 342 public Paint getBackgroundPaint() { 343 return this.backgroundPaint; 344 } 345 346 /** 347 * Sets the background paint and sends a {@link TitleChangeEvent} to all 348 * registered listeners. 349 * 350 * @param paint the paint ({@code null} permitted). 351 */ 352 public void setBackgroundPaint(Paint paint) { 353 this.backgroundPaint = paint; 354 notifyListeners(new TitleChangeEvent(this)); 355 } 356 357 /** 358 * Returns the number of subdivisions used to draw the scale. 359 * 360 * @return The subdivision count. 361 */ 362 public int getSubdivisionCount() { 363 return this.subdivisions; 364 } 365 366 /** 367 * Sets the subdivision count and sends a {@link TitleChangeEvent} to 368 * all registered listeners. 369 * 370 * @param count the count. 371 */ 372 public void setSubdivisionCount(int count) { 373 if (count <= 0) { 374 throw new IllegalArgumentException("Requires 'count' > 0."); 375 } 376 this.subdivisions = count; 377 notifyListeners(new TitleChangeEvent(this)); 378 } 379 380 /** 381 * Receives notification of an axis change event and responds by firing 382 * a title change event. 383 * 384 * @param event the event. 385 */ 386 @Override 387 public void axisChanged(AxisChangeEvent event) { 388 if (this.axis == event.getAxis()) { 389 notifyListeners(new TitleChangeEvent(this)); 390 } 391 } 392 393 /** 394 * Arranges the contents of the block, within the given constraints, and 395 * returns the block size. 396 * 397 * @param g2 the graphics device. 398 * @param constraint the constraint ({@code null} not permitted). 399 * 400 * @return The block size (in Java2D units, never {@code null}). 401 */ 402 @Override 403 public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) { 404 RectangleConstraint cc = toContentConstraint(constraint); 405 LengthConstraintType w = cc.getWidthConstraintType(); 406 LengthConstraintType h = cc.getHeightConstraintType(); 407 Size2D contentSize = null; 408 if (w == LengthConstraintType.NONE) { 409 if (h == LengthConstraintType.NONE) { 410 contentSize = new Size2D(getWidth(), getHeight()); 411 } 412 else if (h == LengthConstraintType.RANGE) { 413 throw new RuntimeException("Not yet implemented."); 414 } 415 else if (h == LengthConstraintType.FIXED) { 416 throw new RuntimeException("Not yet implemented."); 417 } 418 } 419 else if (w == LengthConstraintType.RANGE) { 420 if (h == LengthConstraintType.NONE) { 421 throw new RuntimeException("Not yet implemented."); 422 } 423 else if (h == LengthConstraintType.RANGE) { 424 contentSize = arrangeRR(g2, cc.getWidthRange(), 425 cc.getHeightRange()); 426 } 427 else if (h == LengthConstraintType.FIXED) { 428 throw new RuntimeException("Not yet implemented."); 429 } 430 } 431 else if (w == LengthConstraintType.FIXED) { 432 if (h == LengthConstraintType.NONE) { 433 throw new RuntimeException("Not yet implemented."); 434 } 435 else if (h == LengthConstraintType.RANGE) { 436 throw new RuntimeException("Not yet implemented."); 437 } 438 else if (h == LengthConstraintType.FIXED) { 439 throw new RuntimeException("Not yet implemented."); 440 } 441 } 442 assert contentSize != null; // suppress compiler warning 443 return new Size2D(calculateTotalWidth(contentSize.getWidth()), 444 calculateTotalHeight(contentSize.getHeight())); 445 } 446 447 /** 448 * Returns the content size for the title. This will reflect the fact that 449 * a text title positioned on the left or right of a chart will be rotated 450 * 90 degrees. 451 * 452 * @param g2 the graphics device. 453 * @param widthRange the width range. 454 * @param heightRange the height range. 455 * 456 * @return The content size. 457 */ 458 protected Size2D arrangeRR(Graphics2D g2, Range widthRange, 459 Range heightRange) { 460 461 RectangleEdge position = getPosition(); 462 if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) { 463 464 465 float maxWidth = (float) widthRange.getUpperBound(); 466 467 // determine the space required for the axis 468 AxisSpace space = this.axis.reserveSpace(g2, null, 469 new Rectangle2D.Double(0, 0, maxWidth, 100), 470 RectangleEdge.BOTTOM, null); 471 472 return new Size2D(maxWidth, this.stripWidth + this.axisOffset 473 + space.getTop() + space.getBottom()); 474 } 475 else if (position == RectangleEdge.LEFT || position 476 == RectangleEdge.RIGHT) { 477 float maxHeight = (float) heightRange.getUpperBound(); 478 AxisSpace space = this.axis.reserveSpace(g2, null, 479 new Rectangle2D.Double(0, 0, 100, maxHeight), 480 RectangleEdge.RIGHT, null); 481 return new Size2D(this.stripWidth + this.axisOffset 482 + space.getLeft() + space.getRight(), maxHeight); 483 } 484 else { 485 throw new RuntimeException("Unrecognised position."); 486 } 487 } 488 489 /** 490 * Draws the legend within the specified area. 491 * 492 * @param g2 the graphics target ({@code null} not permitted). 493 * @param area the drawing area ({@code null} not permitted). 494 */ 495 @Override 496 public void draw(Graphics2D g2, Rectangle2D area) { 497 draw(g2, area, null); 498 } 499 500 /** 501 * Draws the legend within the specified area. 502 * 503 * @param g2 the graphics target ({@code null} not permitted). 504 * @param area the drawing area ({@code null} not permitted). 505 * @param params drawing parameters (ignored here). 506 * 507 * @return {@code null}. 508 */ 509 @Override 510 public Object draw(Graphics2D g2, Rectangle2D area, Object params) { 511 Rectangle2D target = (Rectangle2D) area.clone(); 512 target = trimMargin(target); 513 if (this.backgroundPaint != null) { 514 g2.setPaint(this.backgroundPaint); 515 g2.fill(target); 516 } 517 getFrame().draw(g2, target); 518 getFrame().getInsets().trim(target); 519 target = trimPadding(target); 520 double base = this.axis.getLowerBound(); 521 double increment = this.axis.getRange().getLength() / this.subdivisions; 522 Rectangle2D r = new Rectangle2D.Double(); 523 524 if (RectangleEdge.isTopOrBottom(getPosition())) { 525 RectangleEdge axisEdge = Plot.resolveRangeAxisLocation( 526 this.axisLocation, PlotOrientation.HORIZONTAL); 527 if (axisEdge == RectangleEdge.TOP) { 528 for (int i = 0; i < this.subdivisions; i++) { 529 double v = base + (i * increment); 530 Paint p = this.scale.getPaint(v); 531 double vv0 = this.axis.valueToJava2D(v, target, 532 RectangleEdge.TOP); 533 double vv1 = this.axis.valueToJava2D(v + increment, target, 534 RectangleEdge.TOP); 535 double ww = Math.abs(vv1 - vv0) + 1.0; 536 r.setRect(Math.min(vv0, vv1), target.getMaxY() 537 - this.stripWidth, ww, this.stripWidth); 538 g2.setPaint(p); 539 g2.fill(r); 540 } 541 if (isStripOutlineVisible()) { 542 g2.setPaint(this.stripOutlinePaint); 543 g2.setStroke(this.stripOutlineStroke); 544 g2.draw(new Rectangle2D.Double(target.getMinX(), 545 target.getMaxY() - this.stripWidth, 546 target.getWidth(), this.stripWidth)); 547 } 548 this.axis.draw(g2, target.getMaxY() - this.stripWidth 549 - this.axisOffset, target, target, RectangleEdge.TOP, 550 null); 551 } 552 else if (axisEdge == RectangleEdge.BOTTOM) { 553 for (int i = 0; i < this.subdivisions; i++) { 554 double v = base + (i * increment); 555 Paint p = this.scale.getPaint(v); 556 double vv0 = this.axis.valueToJava2D(v, target, 557 RectangleEdge.BOTTOM); 558 double vv1 = this.axis.valueToJava2D(v + increment, target, 559 RectangleEdge.BOTTOM); 560 double ww = Math.abs(vv1 - vv0) + 1.0; 561 r.setRect(Math.min(vv0, vv1), target.getMinY(), ww, 562 this.stripWidth); 563 g2.setPaint(p); 564 g2.fill(r); 565 } 566 if (isStripOutlineVisible()) { 567 g2.setPaint(this.stripOutlinePaint); 568 g2.setStroke(this.stripOutlineStroke); 569 g2.draw(new Rectangle2D.Double(target.getMinX(), 570 target.getMinY(), target.getWidth(), 571 this.stripWidth)); 572 } 573 this.axis.draw(g2, target.getMinY() + this.stripWidth 574 + this.axisOffset, target, target, 575 RectangleEdge.BOTTOM, null); 576 } 577 } 578 else { 579 RectangleEdge axisEdge = Plot.resolveRangeAxisLocation( 580 this.axisLocation, PlotOrientation.VERTICAL); 581 if (axisEdge == RectangleEdge.LEFT) { 582 for (int i = 0; i < this.subdivisions; i++) { 583 double v = base + (i * increment); 584 Paint p = this.scale.getPaint(v); 585 double vv0 = this.axis.valueToJava2D(v, target, 586 RectangleEdge.LEFT); 587 double vv1 = this.axis.valueToJava2D(v + increment, target, 588 RectangleEdge.LEFT); 589 double hh = Math.abs(vv1 - vv0) + 1.0; 590 r.setRect(target.getMaxX() - this.stripWidth, 591 Math.min(vv0, vv1), this.stripWidth, hh); 592 g2.setPaint(p); 593 g2.fill(r); 594 } 595 if (isStripOutlineVisible()) { 596 g2.setPaint(this.stripOutlinePaint); 597 g2.setStroke(this.stripOutlineStroke); 598 g2.draw(new Rectangle2D.Double(target.getMaxX() 599 - this.stripWidth, target.getMinY(), 600 this.stripWidth, target.getHeight())); 601 } 602 this.axis.draw(g2, target.getMaxX() - this.stripWidth 603 - this.axisOffset, target, target, RectangleEdge.LEFT, 604 null); 605 } 606 else if (axisEdge == RectangleEdge.RIGHT) { 607 for (int i = 0; i < this.subdivisions; i++) { 608 double v = base + (i * increment); 609 Paint p = this.scale.getPaint(v); 610 double vv0 = this.axis.valueToJava2D(v, target, 611 RectangleEdge.LEFT); 612 double vv1 = this.axis.valueToJava2D(v + increment, target, 613 RectangleEdge.LEFT); 614 double hh = Math.abs(vv1 - vv0) + 1.0; 615 r.setRect(target.getMinX(), Math.min(vv0, vv1), 616 this.stripWidth, hh); 617 g2.setPaint(p); 618 g2.fill(r); 619 } 620 if (isStripOutlineVisible()) { 621 g2.setPaint(this.stripOutlinePaint); 622 g2.setStroke(this.stripOutlineStroke); 623 g2.draw(new Rectangle2D.Double(target.getMinX(), 624 target.getMinY(), this.stripWidth, 625 target.getHeight())); 626 } 627 this.axis.draw(g2, target.getMinX() + this.stripWidth 628 + this.axisOffset, target, target, RectangleEdge.RIGHT, 629 null); 630 } 631 } 632 return null; 633 } 634 635 /** 636 * Tests this legend for equality with an arbitrary object. 637 * 638 * @param obj the object ({@code null} permitted). 639 * 640 * @return A boolean. 641 */ 642 @Override 643 public boolean equals(Object obj) { 644 if (!(obj instanceof PaintScaleLegend)) { 645 return false; 646 } 647 PaintScaleLegend that = (PaintScaleLegend) obj; 648 if (!this.scale.equals(that.scale)) { 649 return false; 650 } 651 if (!this.axis.equals(that.axis)) { 652 return false; 653 } 654 if (!this.axisLocation.equals(that.axisLocation)) { 655 return false; 656 } 657 if (this.axisOffset != that.axisOffset) { 658 return false; 659 } 660 if (this.stripWidth != that.stripWidth) { 661 return false; 662 } 663 if (this.stripOutlineVisible != that.stripOutlineVisible) { 664 return false; 665 } 666 if (!PaintUtils.equal(this.stripOutlinePaint, 667 that.stripOutlinePaint)) { 668 return false; 669 } 670 if (!this.stripOutlineStroke.equals(that.stripOutlineStroke)) { 671 return false; 672 } 673 if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) { 674 return false; 675 } 676 if (this.subdivisions != that.subdivisions) { 677 return false; 678 } 679 return super.equals(obj); 680 } 681 682 /** 683 * Provides serialization support. 684 * 685 * @param stream the output stream. 686 * 687 * @throws IOException if there is an I/O error. 688 */ 689 private void writeObject(ObjectOutputStream stream) throws IOException { 690 stream.defaultWriteObject(); 691 SerialUtils.writePaint(this.backgroundPaint, stream); 692 SerialUtils.writePaint(this.stripOutlinePaint, stream); 693 SerialUtils.writeStroke(this.stripOutlineStroke, stream); 694 } 695 696 /** 697 * Provides serialization support. 698 * 699 * @param stream the input stream. 700 * 701 * @throws IOException if there is an I/O error. 702 * @throws ClassNotFoundException if there is a classpath problem. 703 */ 704 private void readObject(ObjectInputStream stream) 705 throws IOException, ClassNotFoundException { 706 stream.defaultReadObject(); 707 this.backgroundPaint = SerialUtils.readPaint(stream); 708 this.stripOutlinePaint = SerialUtils.readPaint(stream); 709 this.stripOutlineStroke = SerialUtils.readStroke(stream); 710 } 711 712}