001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, by David Gilbert 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 * DialPointer.java 029 * ---------------- 030 * (C) Copyright 2006-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.chart.plot.dial; 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.Arc2D; 045import java.awt.geom.GeneralPath; 046import java.awt.geom.Line2D; 047import java.awt.geom.Point2D; 048import java.awt.geom.Rectangle2D; 049import java.io.IOException; 050import java.io.ObjectInputStream; 051import java.io.ObjectOutputStream; 052import java.io.Serializable; 053import org.jfree.chart.internal.HashUtils; 054import org.jfree.chart.internal.PaintUtils; 055import org.jfree.chart.internal.Args; 056import org.jfree.chart.api.PublicCloneable; 057import org.jfree.chart.internal.SerialUtils; 058 059/** 060 * A base class for the pointer in a {@link DialPlot}. 061 */ 062public abstract class DialPointer extends AbstractDialLayer 063 implements DialLayer, Cloneable, PublicCloneable, Serializable { 064 065 /** The needle radius. */ 066 double radius; 067 068 /** 069 * The dataset index for the needle. 070 */ 071 int datasetIndex; 072 073 /** 074 * Creates a new {@code DialPointer} instance. 075 */ 076 protected DialPointer() { 077 this(0); 078 } 079 080 /** 081 * Creates a new pointer for the specified dataset. 082 * 083 * @param datasetIndex the dataset index. 084 */ 085 protected DialPointer(int datasetIndex) { 086 this.radius = 0.9; 087 this.datasetIndex = datasetIndex; 088 } 089 090 /** 091 * Returns the dataset index that the pointer maps to. 092 * 093 * @return The dataset index. 094 * 095 * @see #getDatasetIndex() 096 */ 097 public int getDatasetIndex() { 098 return this.datasetIndex; 099 } 100 101 /** 102 * Sets the dataset index for the pointer and sends a 103 * {@link DialLayerChangeEvent} to all registered listeners. 104 * 105 * @param index the index. 106 * 107 * @see #getDatasetIndex() 108 */ 109 public void setDatasetIndex(int index) { 110 this.datasetIndex = index; 111 notifyListeners(new DialLayerChangeEvent(this)); 112 } 113 114 /** 115 * Returns the radius of the pointer, as a percentage of the dial's 116 * framing rectangle. 117 * 118 * @return The radius. 119 * 120 * @see #setRadius(double) 121 */ 122 public double getRadius() { 123 return this.radius; 124 } 125 126 /** 127 * Sets the radius of the pointer and sends a 128 * {@link DialLayerChangeEvent} to all registered listeners. 129 * 130 * @param radius the radius. 131 * 132 * @see #getRadius() 133 */ 134 public void setRadius(double radius) { 135 this.radius = radius; 136 notifyListeners(new DialLayerChangeEvent(this)); 137 } 138 139 /** 140 * Returns {@code true} to indicate that this layer should be 141 * clipped within the dial window. 142 * 143 * @return {@code true}. 144 */ 145 @Override 146 public boolean isClippedToWindow() { 147 return true; 148 } 149 150 /** 151 * Checks this instance for equality with an arbitrary object. 152 * 153 * @param obj the object ({@code null} not permitted). 154 * 155 * @return A boolean. 156 */ 157 @Override 158 public boolean equals(Object obj) { 159 if (obj == this) { 160 return true; 161 } 162 if (!(obj instanceof DialPointer)) { 163 return false; 164 } 165 DialPointer that = (DialPointer) obj; 166 if (this.datasetIndex != that.datasetIndex) { 167 return false; 168 } 169 if (this.radius != that.radius) { 170 return false; 171 } 172 return super.equals(obj); 173 } 174 175 /** 176 * Returns a hash code. 177 * 178 * @return A hash code. 179 */ 180 @Override 181 public int hashCode() { 182 int result = 23; 183 result = HashUtils.hashCode(result, this.radius); 184 return result; 185 } 186 187 /** 188 * Returns a clone of the pointer. 189 * 190 * @return a clone. 191 * 192 * @throws CloneNotSupportedException if one of the attributes cannot 193 * be cloned. 194 */ 195 @Override 196 public Object clone() throws CloneNotSupportedException { 197 return super.clone(); 198 } 199 200 /** 201 * A dial pointer that draws a thin line (like a pin). 202 */ 203 public static class Pin extends DialPointer { 204 205 /** For serialization. */ 206 static final long serialVersionUID = -8445860485367689750L; 207 208 /** The paint. */ 209 private transient Paint paint; 210 211 /** The stroke. */ 212 private transient Stroke stroke; 213 214 /** 215 * Creates a new instance. 216 */ 217 public Pin() { 218 this(0); 219 } 220 221 /** 222 * Creates a new instance. 223 * 224 * @param datasetIndex the dataset index. 225 */ 226 public Pin(int datasetIndex) { 227 super(datasetIndex); 228 this.paint = Color.RED; 229 this.stroke = new BasicStroke(3.0f, BasicStroke.CAP_ROUND, 230 BasicStroke.JOIN_BEVEL); 231 } 232 233 /** 234 * Returns the paint. 235 * 236 * @return The paint (never {@code null}). 237 * 238 * @see #setPaint(Paint) 239 */ 240 public Paint getPaint() { 241 return this.paint; 242 } 243 244 /** 245 * Sets the paint and sends a {@link DialLayerChangeEvent} to all 246 * registered listeners. 247 * 248 * @param paint the paint ({@code null} not permitted). 249 * 250 * @see #getPaint() 251 */ 252 public void setPaint(Paint paint) { 253 Args.nullNotPermitted(paint, "paint"); 254 this.paint = paint; 255 notifyListeners(new DialLayerChangeEvent(this)); 256 } 257 258 /** 259 * Returns the stroke. 260 * 261 * @return The stroke (never {@code null}). 262 * 263 * @see #setStroke(Stroke) 264 */ 265 public Stroke getStroke() { 266 return this.stroke; 267 } 268 269 /** 270 * Sets the stroke and sends a {@link DialLayerChangeEvent} to all 271 * registered listeners. 272 * 273 * @param stroke the stroke ({@code null} not permitted). 274 * 275 * @see #getStroke() 276 */ 277 public void setStroke(Stroke stroke) { 278 Args.nullNotPermitted(stroke, "stroke"); 279 this.stroke = stroke; 280 notifyListeners(new DialLayerChangeEvent(this)); 281 } 282 283 /** 284 * Draws the pointer. 285 * 286 * @param g2 the graphics target. 287 * @param plot the plot. 288 * @param frame the dial's reference frame. 289 * @param view the dial's view. 290 */ 291 @Override 292 public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame, 293 Rectangle2D view) { 294 295 g2.setPaint(this.paint); 296 g2.setStroke(this.stroke); 297 Rectangle2D arcRect = DialPlot.rectangleByRadius(frame, 298 this.radius, this.radius); 299 300 double value = plot.getValue(this.datasetIndex); 301 DialScale scale = plot.getScaleForDataset(this.datasetIndex); 302 double angle = scale.valueToAngle(value); 303 304 Arc2D arc = new Arc2D.Double(arcRect, angle, 0, Arc2D.OPEN); 305 Point2D pt = arc.getEndPoint(); 306 307 Line2D line = new Line2D.Double(frame.getCenterX(), 308 frame.getCenterY(), pt.getX(), pt.getY()); 309 g2.draw(line); 310 } 311 312 /** 313 * Tests this pointer for equality with an arbitrary object. 314 * 315 * @param obj the object ({@code null} permitted). 316 * 317 * @return A boolean. 318 */ 319 @Override 320 public boolean equals(Object obj) { 321 if (obj == this) { 322 return true; 323 } 324 if (!(obj instanceof DialPointer.Pin)) { 325 return false; 326 } 327 DialPointer.Pin that = (DialPointer.Pin) obj; 328 if (!PaintUtils.equal(this.paint, that.paint)) { 329 return false; 330 } 331 if (!this.stroke.equals(that.stroke)) { 332 return false; 333 } 334 return super.equals(obj); 335 } 336 337 /** 338 * Returns a hash code for this instance. 339 * 340 * @return A hash code. 341 */ 342 @Override 343 public int hashCode() { 344 int result = super.hashCode(); 345 result = HashUtils.hashCode(result, this.paint); 346 result = HashUtils.hashCode(result, this.stroke); 347 return result; 348 } 349 350 /** 351 * Provides serialization support. 352 * 353 * @param stream the output stream. 354 * 355 * @throws IOException if there is an I/O error. 356 */ 357 private void writeObject(ObjectOutputStream stream) throws IOException { 358 stream.defaultWriteObject(); 359 SerialUtils.writePaint(this.paint, stream); 360 SerialUtils.writeStroke(this.stroke, stream); 361 } 362 363 /** 364 * Provides serialization support. 365 * 366 * @param stream the input stream. 367 * 368 * @throws IOException if there is an I/O error. 369 * @throws ClassNotFoundException if there is a classpath problem. 370 */ 371 private void readObject(ObjectInputStream stream) 372 throws IOException, ClassNotFoundException { 373 stream.defaultReadObject(); 374 this.paint = SerialUtils.readPaint(stream); 375 this.stroke = SerialUtils.readStroke(stream); 376 } 377 378 } 379 380 /** 381 * A dial pointer. 382 */ 383 public static class Pointer extends DialPointer { 384 385 /** For serialization. */ 386 static final long serialVersionUID = -4180500011963176960L; 387 388 /** 389 * The radius that defines the width of the pointer at the base. 390 */ 391 private double widthRadius; 392 393 /** The fill paint. */ 394 private transient Paint fillPaint; 395 396 /** The outline paint. */ 397 private transient Paint outlinePaint; 398 399 /** 400 * Creates a new instance. 401 */ 402 public Pointer() { 403 this(0); 404 } 405 406 /** 407 * Creates a new instance. 408 * 409 * @param datasetIndex the dataset index. 410 */ 411 public Pointer(int datasetIndex) { 412 super(datasetIndex); 413 this.widthRadius = 0.05; 414 this.fillPaint = Color.GRAY; 415 this.outlinePaint = Color.BLACK; 416 } 417 418 /** 419 * Returns the width radius. 420 * 421 * @return The width radius. 422 * 423 * @see #setWidthRadius(double) 424 */ 425 public double getWidthRadius() { 426 return this.widthRadius; 427 } 428 429 /** 430 * Sets the width radius and sends a {@link DialLayerChangeEvent} to 431 * all registered listeners. 432 * 433 * @param radius the radius 434 * 435 * @see #getWidthRadius() 436 */ 437 public void setWidthRadius(double radius) { 438 this.widthRadius = radius; 439 notifyListeners(new DialLayerChangeEvent(this)); 440 } 441 442 /** 443 * Returns the fill paint. 444 * 445 * @return The paint (never {@code null}). 446 * 447 * @see #setFillPaint(Paint) 448 */ 449 public Paint getFillPaint() { 450 return this.fillPaint; 451 } 452 453 /** 454 * Sets the fill paint and sends a {@link DialLayerChangeEvent} to all 455 * registered listeners. 456 * 457 * @param paint the paint ({@code null} not permitted). 458 * 459 * @see #getFillPaint() 460 */ 461 public void setFillPaint(Paint paint) { 462 Args.nullNotPermitted(paint, "paint"); 463 this.fillPaint = paint; 464 notifyListeners(new DialLayerChangeEvent(this)); 465 } 466 467 /** 468 * Returns the outline paint. 469 * 470 * @return The paint (never {@code null}). 471 * 472 * @see #setOutlinePaint(Paint) 473 */ 474 public Paint getOutlinePaint() { 475 return this.outlinePaint; 476 } 477 478 /** 479 * Sets the outline paint and sends a {@link DialLayerChangeEvent} to 480 * all registered listeners. 481 * 482 * @param paint the paint ({@code null} not permitted). 483 * 484 * @see #getOutlinePaint() 485 */ 486 public void setOutlinePaint(Paint paint) { 487 Args.nullNotPermitted(paint, "paint"); 488 this.outlinePaint = paint; 489 notifyListeners(new DialLayerChangeEvent(this)); 490 } 491 492 /** 493 * Draws the pointer. 494 * 495 * @param g2 the graphics target. 496 * @param plot the plot. 497 * @param frame the dial's reference frame. 498 * @param view the dial's view. 499 */ 500 @Override 501 public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame, 502 Rectangle2D view) { 503 504 g2.setPaint(Color.BLUE); 505 g2.setStroke(new BasicStroke(1.0f)); 506 Rectangle2D lengthRect = DialPlot.rectangleByRadius(frame, 507 this.radius, this.radius); 508 Rectangle2D widthRect = DialPlot.rectangleByRadius(frame, 509 this.widthRadius, this.widthRadius); 510 double value = plot.getValue(this.datasetIndex); 511 DialScale scale = plot.getScaleForDataset(this.datasetIndex); 512 double angle = scale.valueToAngle(value); 513 514 Arc2D arc1 = new Arc2D.Double(lengthRect, angle, 0, Arc2D.OPEN); 515 Point2D pt1 = arc1.getEndPoint(); 516 Arc2D arc2 = new Arc2D.Double(widthRect, angle - 90.0, 180.0, 517 Arc2D.OPEN); 518 Point2D pt2 = arc2.getStartPoint(); 519 Point2D pt3 = arc2.getEndPoint(); 520 Arc2D arc3 = new Arc2D.Double(widthRect, angle - 180.0, 0.0, 521 Arc2D.OPEN); 522 Point2D pt4 = arc3.getStartPoint(); 523 524 GeneralPath gp = new GeneralPath(); 525 gp.moveTo((float) pt1.getX(), (float) pt1.getY()); 526 gp.lineTo((float) pt2.getX(), (float) pt2.getY()); 527 gp.lineTo((float) pt4.getX(), (float) pt4.getY()); 528 gp.lineTo((float) pt3.getX(), (float) pt3.getY()); 529 gp.closePath(); 530 g2.setPaint(this.fillPaint); 531 g2.fill(gp); 532 533 g2.setPaint(this.outlinePaint); 534 Line2D line = new Line2D.Double(frame.getCenterX(), 535 frame.getCenterY(), pt1.getX(), pt1.getY()); 536 g2.draw(line); 537 538 line.setLine(pt2, pt3); 539 g2.draw(line); 540 541 line.setLine(pt3, pt1); 542 g2.draw(line); 543 544 line.setLine(pt2, pt1); 545 g2.draw(line); 546 547 line.setLine(pt2, pt4); 548 g2.draw(line); 549 550 line.setLine(pt3, pt4); 551 g2.draw(line); 552 } 553 554 /** 555 * Tests this pointer for equality with an arbitrary object. 556 * 557 * @param obj the object ({@code null} permitted). 558 * 559 * @return A boolean. 560 */ 561 @Override 562 public boolean equals(Object obj) { 563 if (obj == this) { 564 return true; 565 } 566 if (!(obj instanceof DialPointer.Pointer)) { 567 return false; 568 } 569 DialPointer.Pointer that = (DialPointer.Pointer) obj; 570 571 if (this.widthRadius != that.widthRadius) { 572 return false; 573 } 574 if (!PaintUtils.equal(this.fillPaint, that.fillPaint)) { 575 return false; 576 } 577 if (!PaintUtils.equal(this.outlinePaint, that.outlinePaint)) { 578 return false; 579 } 580 return super.equals(obj); 581 } 582 583 /** 584 * Returns a hash code for this instance. 585 * 586 * @return A hash code. 587 */ 588 @Override 589 public int hashCode() { 590 int result = super.hashCode(); 591 result = HashUtils.hashCode(result, this.widthRadius); 592 result = HashUtils.hashCode(result, this.fillPaint); 593 result = HashUtils.hashCode(result, this.outlinePaint); 594 return result; 595 } 596 597 /** 598 * Provides serialization support. 599 * 600 * @param stream the output stream. 601 * 602 * @throws IOException if there is an I/O error. 603 */ 604 private void writeObject(ObjectOutputStream stream) throws IOException { 605 stream.defaultWriteObject(); 606 SerialUtils.writePaint(this.fillPaint, stream); 607 SerialUtils.writePaint(this.outlinePaint, stream); 608 } 609 610 /** 611 * Provides serialization support. 612 * 613 * @param stream the input stream. 614 * 615 * @throws IOException if there is an I/O error. 616 * @throws ClassNotFoundException if there is a classpath problem. 617 */ 618 private void readObject(ObjectInputStream stream) 619 throws IOException, ClassNotFoundException { 620 stream.defaultReadObject(); 621 this.fillPaint = SerialUtils.readPaint(stream); 622 this.outlinePaint = SerialUtils.readPaint(stream); 623 } 624 625 } 626 627}