001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, 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 * TaskSeriesCollection.java 029 * ------------------------- 030 * (C) Copyright 2002-present, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Thomas Schuster; 034 * Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier); 035 * 036 */ 037 038package org.jfree.data.gantt; 039 040import org.jfree.chart.util.Args; 041import org.jfree.chart.util.ObjectUtils; 042import org.jfree.chart.util.PublicCloneable; 043import org.jfree.data.general.AbstractSeriesDataset; 044import org.jfree.data.general.SeriesChangeEvent; 045import org.jfree.data.time.TimePeriod; 046 047import java.io.IOException; 048import java.io.ObjectInputStream; 049import java.io.ObjectOutputStream; 050import java.io.Serializable; 051import java.util.List; 052import java.util.Objects; 053 054/** 055 * A collection of {@link TaskSeries} objects. This class provides one 056 * implementation of the {@link GanttCategoryDataset} interface. 057 */ 058public class TaskSeriesCollection extends AbstractSeriesDataset 059 implements GanttCategoryDataset, Cloneable, PublicCloneable, 060 Serializable { 061 062 /** For serialization. */ 063 private static final long serialVersionUID = -2065799050738449903L; 064 065 /** 066 * Storage for aggregate task keys (the task description is used as the 067 * key). 068 */ 069 private List keys; 070 071 /** Storage for the series. */ 072 private List data; 073 074 /** 075 * Default constructor. 076 */ 077 public TaskSeriesCollection() { 078 this.keys = new java.util.ArrayList(); 079 this.data = new java.util.ArrayList(); 080 } 081 082 /** 083 * Returns a series from the collection. 084 * 085 * @param key the series key ({@code null} not permitted). 086 * 087 * @return The series. 088 */ 089 public TaskSeries getSeries(Comparable key) { 090 if (key == null) { 091 throw new NullPointerException("Null 'key' argument."); 092 } 093 TaskSeries result = null; 094 int index = getRowIndex(key); 095 if (index >= 0) { 096 result = getSeries(index); 097 } 098 return result; 099 } 100 101 /** 102 * Returns a series from the collection. 103 * 104 * @param series the series index (zero-based). 105 * 106 * @return The series. 107 */ 108 public TaskSeries getSeries(int series) { 109 if ((series < 0) || (series >= getSeriesCount())) { 110 throw new IllegalArgumentException("Series index out of bounds"); 111 } 112 return (TaskSeries) this.data.get(series); 113 } 114 115 /** 116 * Returns the number of series in the collection. 117 * 118 * @return The series count. 119 */ 120 @Override 121 public int getSeriesCount() { 122 return getRowCount(); 123 } 124 125 /** 126 * Returns the name of a series. 127 * 128 * @param series the series index (zero-based). 129 * 130 * @return The name of a series. 131 */ 132 @Override 133 public Comparable getSeriesKey(int series) { 134 TaskSeries ts = (TaskSeries) this.data.get(series); 135 return ts.getKey(); 136 } 137 138 /** 139 * Returns the number of rows (series) in the collection. 140 * 141 * @return The series count. 142 */ 143 @Override 144 public int getRowCount() { 145 return this.data.size(); 146 } 147 148 /** 149 * Returns the row keys. In this case, each series is a key. 150 * 151 * @return The row keys. 152 */ 153 @Override 154 public List getRowKeys() { 155 return this.data; 156 } 157 158 /** 159 * Returns the number of column in the dataset. 160 * 161 * @return The column count. 162 */ 163 @Override 164 public int getColumnCount() { 165 return this.keys.size(); 166 } 167 168 /** 169 * Returns a list of the column keys in the dataset. 170 * 171 * @return The category list. 172 */ 173 @Override 174 public List getColumnKeys() { 175 return this.keys; 176 } 177 178 /** 179 * Returns a column key. 180 * 181 * @param index the column index. 182 * 183 * @return The column key. 184 */ 185 @Override 186 public Comparable getColumnKey(int index) { 187 return (Comparable) this.keys.get(index); 188 } 189 190 /** 191 * Returns the column index for a column key. 192 * 193 * @param columnKey the column key ({@code null} not permitted). 194 * 195 * @return The column index. 196 */ 197 @Override 198 public int getColumnIndex(Comparable columnKey) { 199 Args.nullNotPermitted(columnKey, "columnKey"); 200 return this.keys.indexOf(columnKey); 201 } 202 203 /** 204 * Returns the row index for the given row key. 205 * 206 * @param rowKey the row key. 207 * 208 * @return The index. 209 */ 210 @Override 211 public int getRowIndex(Comparable rowKey) { 212 int result = -1; 213 int count = this.data.size(); 214 for (int i = 0; i < count; i++) { 215 TaskSeries s = (TaskSeries) this.data.get(i); 216 if (s.getKey().equals(rowKey)) { 217 result = i; 218 break; 219 } 220 } 221 return result; 222 } 223 224 /** 225 * Returns the key for a row. 226 * 227 * @param index the row index (zero-based). 228 * 229 * @return The key. 230 */ 231 @Override 232 public Comparable getRowKey(int index) { 233 TaskSeries series = (TaskSeries) this.data.get(index); 234 return series.getKey(); 235 } 236 237 /** 238 * Adds a series to the dataset and sends a 239 * {@link org.jfree.data.general.DatasetChangeEvent} to all registered 240 * listeners. 241 * 242 * @param series the series ({@code null} not permitted). 243 */ 244 public void add(TaskSeries series) { 245 Args.nullNotPermitted(series, "series"); 246 this.data.add(series); 247 series.addChangeListener(this); 248 249 // look for any keys that we don't already know about... 250 for (Object o : series.getTasks()) { 251 Task task = (Task) o; 252 String key = task.getDescription(); 253 int index = this.keys.indexOf(key); 254 if (index < 0) { 255 this.keys.add(key); 256 } 257 } 258 fireDatasetChanged(); 259 } 260 261 /** 262 * Removes a series from the collection and sends 263 * a {@link org.jfree.data.general.DatasetChangeEvent} 264 * to all registered listeners. 265 * 266 * @param series the series. 267 */ 268 public void remove(TaskSeries series) { 269 Args.nullNotPermitted(series, "series"); 270 if (this.data.contains(series)) { 271 series.removeChangeListener(this); 272 this.data.remove(series); 273 fireDatasetChanged(); 274 } 275 } 276 277 /** 278 * Removes a series from the collection and sends 279 * a {@link org.jfree.data.general.DatasetChangeEvent} 280 * to all registered listeners. 281 * 282 * @param series the series (zero based index). 283 */ 284 public void remove(int series) { 285 if ((series < 0) || (series >= getSeriesCount())) { 286 throw new IllegalArgumentException( 287 "TaskSeriesCollection.remove(): index outside valid range."); 288 } 289 290 // fetch the series, remove the change listener, then remove the series. 291 TaskSeries ts = (TaskSeries) this.data.get(series); 292 ts.removeChangeListener(this); 293 this.data.remove(series); 294 fireDatasetChanged(); 295 296 } 297 298 /** 299 * Removes all the series from the collection and sends 300 * a {@link org.jfree.data.general.DatasetChangeEvent} 301 * to all registered listeners. 302 */ 303 public void removeAll() { 304 305 // deregister the collection as a change listener to each series in 306 // the collection. 307 for (Object item : this.data) { 308 TaskSeries series = (TaskSeries) item; 309 series.removeChangeListener(this); 310 } 311 312 // remove all the series from the collection and notify listeners. 313 this.data.clear(); 314 fireDatasetChanged(); 315 316 } 317 318 /** 319 * Returns the value for an item. 320 * 321 * @param rowKey the row key. 322 * @param columnKey the column key. 323 * 324 * @return The item value. 325 */ 326 @Override 327 public Number getValue(Comparable rowKey, Comparable columnKey) { 328 return getStartValue(rowKey, columnKey); 329 } 330 331 /** 332 * Returns the value for a task. 333 * 334 * @param row the row index (zero-based). 335 * @param column the column index (zero-based). 336 * 337 * @return The start value. 338 */ 339 @Override 340 public Number getValue(int row, int column) { 341 return getStartValue(row, column); 342 } 343 344 /** 345 * Returns the start value for a task. This is a date/time value, measured 346 * in milliseconds since 1-Jan-1970. 347 * 348 * @param rowKey the series. 349 * @param columnKey the category. 350 * 351 * @return The start value (possibly {@code null}). 352 */ 353 @Override 354 public Number getStartValue(Comparable rowKey, Comparable columnKey) { 355 Number result = null; 356 int row = getRowIndex(rowKey); 357 TaskSeries series = (TaskSeries) this.data.get(row); 358 Task task = series.get(columnKey.toString()); 359 if (task != null) { 360 TimePeriod duration = task.getDuration(); 361 if (duration != null) { 362 result = duration.getStart().getTime(); 363 } 364 } 365 return result; 366 } 367 368 /** 369 * Returns the start value for a task. 370 * 371 * @param row the row index (zero-based). 372 * @param column the column index (zero-based). 373 * 374 * @return The start value. 375 */ 376 @Override 377 public Number getStartValue(int row, int column) { 378 Comparable rowKey = getRowKey(row); 379 Comparable columnKey = getColumnKey(column); 380 return getStartValue(rowKey, columnKey); 381 } 382 383 /** 384 * Returns the end value for a task. This is a date/time value, measured 385 * in milliseconds since 1-Jan-1970. 386 * 387 * @param rowKey the series. 388 * @param columnKey the category. 389 * 390 * @return The end value (possibly {@code null}). 391 */ 392 @Override 393 public Number getEndValue(Comparable rowKey, Comparable columnKey) { 394 Number result = null; 395 int row = getRowIndex(rowKey); 396 TaskSeries series = (TaskSeries) this.data.get(row); 397 Task task = series.get(columnKey.toString()); 398 if (task != null) { 399 TimePeriod duration = task.getDuration(); 400 if (duration != null) { 401 result = duration.getEnd().getTime(); 402 } 403 } 404 return result; 405 } 406 407 /** 408 * Returns the end value for a task. 409 * 410 * @param row the row index (zero-based). 411 * @param column the column index (zero-based). 412 * 413 * @return The end value. 414 */ 415 @Override 416 public Number getEndValue(int row, int column) { 417 Comparable rowKey = getRowKey(row); 418 Comparable columnKey = getColumnKey(column); 419 return getEndValue(rowKey, columnKey); 420 } 421 422 /** 423 * Returns the percent complete for a given item. 424 * 425 * @param row the row index (zero-based). 426 * @param column the column index (zero-based). 427 * 428 * @return The percent complete (possibly {@code null}). 429 */ 430 @Override 431 public Number getPercentComplete(int row, int column) { 432 Comparable rowKey = getRowKey(row); 433 Comparable columnKey = getColumnKey(column); 434 return getPercentComplete(rowKey, columnKey); 435 } 436 437 /** 438 * Returns the percent complete for a given item. 439 * 440 * @param rowKey the row key. 441 * @param columnKey the column key. 442 * 443 * @return The percent complete. 444 */ 445 @Override 446 public Number getPercentComplete(Comparable rowKey, Comparable columnKey) { 447 Number result = null; 448 int row = getRowIndex(rowKey); 449 TaskSeries series = (TaskSeries) this.data.get(row); 450 Task task = series.get(columnKey.toString()); 451 if (task != null) { 452 result = task.getPercentComplete(); 453 } 454 return result; 455 } 456 457 /** 458 * Returns the number of sub-intervals for a given item. 459 * 460 * @param row the row index (zero-based). 461 * @param column the column index (zero-based). 462 * 463 * @return The sub-interval count. 464 */ 465 @Override 466 public int getSubIntervalCount(int row, int column) { 467 Comparable rowKey = getRowKey(row); 468 Comparable columnKey = getColumnKey(column); 469 return getSubIntervalCount(rowKey, columnKey); 470 } 471 472 /** 473 * Returns the number of sub-intervals for a given item. 474 * 475 * @param rowKey the row key. 476 * @param columnKey the column key. 477 * 478 * @return The sub-interval count. 479 */ 480 @Override 481 public int getSubIntervalCount(Comparable rowKey, Comparable columnKey) { 482 int result = 0; 483 int row = getRowIndex(rowKey); 484 TaskSeries series = (TaskSeries) this.data.get(row); 485 Task task = series.get(columnKey.toString()); 486 if (task != null) { 487 result = task.getSubtaskCount(); 488 } 489 return result; 490 } 491 492 /** 493 * Returns the start value of a sub-interval for a given item. 494 * 495 * @param row the row index (zero-based). 496 * @param column the column index (zero-based). 497 * @param subinterval the sub-interval index (zero-based). 498 * 499 * @return The start value (possibly {@code null}). 500 */ 501 @Override 502 public Number getStartValue(int row, int column, int subinterval) { 503 Comparable rowKey = getRowKey(row); 504 Comparable columnKey = getColumnKey(column); 505 return getStartValue(rowKey, columnKey, subinterval); 506 } 507 508 /** 509 * Returns the start value of a sub-interval for a given item. 510 * 511 * @param rowKey the row key. 512 * @param columnKey the column key. 513 * @param subinterval the subinterval. 514 * 515 * @return The start value (possibly {@code null}). 516 */ 517 @Override 518 public Number getStartValue(Comparable rowKey, Comparable columnKey, 519 int subinterval) { 520 Number result = null; 521 int row = getRowIndex(rowKey); 522 TaskSeries series = (TaskSeries) this.data.get(row); 523 Task task = series.get(columnKey.toString()); 524 if (task != null) { 525 Task sub = task.getSubtask(subinterval); 526 if (sub != null) { 527 TimePeriod duration = sub.getDuration(); 528 result = duration.getStart().getTime(); 529 } 530 } 531 return result; 532 } 533 534 /** 535 * Returns the end value of a sub-interval for a given item. 536 * 537 * @param row the row index (zero-based). 538 * @param column the column index (zero-based). 539 * @param subinterval the subinterval. 540 * 541 * @return The end value (possibly {@code null}). 542 */ 543 @Override 544 public Number getEndValue(int row, int column, int subinterval) { 545 Comparable rowKey = getRowKey(row); 546 Comparable columnKey = getColumnKey(column); 547 return getEndValue(rowKey, columnKey, subinterval); 548 } 549 550 /** 551 * Returns the end value of a sub-interval for a given item. 552 * 553 * @param rowKey the row key. 554 * @param columnKey the column key. 555 * @param subinterval the subinterval. 556 * 557 * @return The end value (possibly {@code null}). 558 */ 559 @Override 560 public Number getEndValue(Comparable rowKey, Comparable columnKey, 561 int subinterval) { 562 Number result = null; 563 int row = getRowIndex(rowKey); 564 TaskSeries series = (TaskSeries) this.data.get(row); 565 Task task = series.get(columnKey.toString()); 566 if (task != null) { 567 Task sub = task.getSubtask(subinterval); 568 if (sub != null) { 569 TimePeriod duration = sub.getDuration(); 570 result = duration.getEnd().getTime(); 571 } 572 } 573 return result; 574 } 575 576 /** 577 * Returns the percentage complete value of a sub-interval for a given item. 578 * 579 * @param row the row index (zero-based). 580 * @param column the column index (zero-based). 581 * @param subinterval the sub-interval. 582 * 583 * @return The percent complete value (possibly {@code null}). 584 */ 585 @Override 586 public Number getPercentComplete(int row, int column, int subinterval) { 587 Comparable rowKey = getRowKey(row); 588 Comparable columnKey = getColumnKey(column); 589 return getPercentComplete(rowKey, columnKey, subinterval); 590 } 591 592 /** 593 * Returns the percentage complete value of a sub-interval for a given item. 594 * 595 * @param rowKey the row key. 596 * @param columnKey the column key. 597 * @param subinterval the sub-interval. 598 * 599 * @return The percent complete value (possibly {@code null}). 600 */ 601 @Override 602 public Number getPercentComplete(Comparable rowKey, Comparable columnKey, 603 int subinterval) { 604 Number result = null; 605 int row = getRowIndex(rowKey); 606 TaskSeries series = (TaskSeries) this.data.get(row); 607 Task task = series.get(columnKey.toString()); 608 if (task != null) { 609 Task sub = task.getSubtask(subinterval); 610 if (sub != null) { 611 result = sub.getPercentComplete(); 612 } 613 } 614 return result; 615 } 616 617 /** 618 * Called when a series belonging to the dataset changes. 619 * 620 * @param event information about the change. 621 */ 622 @Override 623 public void seriesChanged(SeriesChangeEvent event) { 624 refreshKeys(); 625 fireDatasetChanged(); 626 } 627 628 /** 629 * Refreshes the keys. 630 */ 631 private void refreshKeys() { 632 633 this.keys.clear(); 634 for (int i = 0; i < getSeriesCount(); i++) { 635 TaskSeries series = (TaskSeries) this.data.get(i); 636 // look for any keys that we don't already know about... 637 for (Object o : series.getTasks()) { 638 Task task = (Task) o; 639 String key = task.getDescription(); 640 int index = this.keys.indexOf(key); 641 if (index < 0) { 642 this.keys.add(key); 643 } 644 } 645 } 646 647 } 648 649 /** 650 * Tests this instance for equality with an arbitrary object. 651 * 652 * @param obj the object ({@code null} permitted). 653 * 654 * @return A boolean. 655 */ 656 @Override 657 public boolean equals(Object obj) { 658 if (obj == this) { 659 return true; 660 } 661 if (!(obj instanceof TaskSeriesCollection)) { 662 return false; 663 } 664 TaskSeriesCollection that = (TaskSeriesCollection) obj; 665 if (!Objects.equals(this.data, that.data)) { 666 return false; 667 } 668 if (!Objects.equals(this.keys, that.keys)) { 669 return false; 670 } 671 if (!that.canEqual(this)) { 672 return false; 673 } 674 return super.equals(obj); 675 } 676 677 /** 678 * Ensures symmetry between super/subclass implementations of equals. For 679 * more detail, see http://jqno.nl/equalsverifier/manual/inheritance. 680 * 681 * @param other Object 682 * 683 * @return true ONLY if the parameter is THIS class type 684 */ 685 @Override 686 public boolean canEqual(Object other) { 687 // fix the "equals not symmetric" problem 688 return (other instanceof TaskSeriesCollection); 689 } 690 691 @Override 692 public int hashCode() { 693 int hash = super.hashCode(); // equals calls superclass, hashCode must also 694 hash = 79 * hash + Objects.hashCode(this.data); 695 hash = 79 * hash + Objects.hashCode(this.keys); 696 return hash; 697 } 698 699 /** 700 * Returns an independent copy of this dataset. 701 * 702 * @return A clone of the dataset. 703 * 704 * @throws CloneNotSupportedException if there is some problem cloning 705 * the dataset. 706 */ 707 @Override 708 public Object clone() throws CloneNotSupportedException { 709 TaskSeriesCollection clone = (TaskSeriesCollection) super.clone(); 710 clone.data = (List) ObjectUtils.deepClone(this.data); 711 clone.keys = new java.util.ArrayList(this.keys); 712 return clone; 713 } 714 715 /** 716 * Provides serialization support. 717 * 718 * @param stream the output stream. 719 * 720 * @throws IOException if there is an I/O error. 721 */ 722 private void writeObject(ObjectOutputStream stream) throws IOException { 723 stream.defaultWriteObject(); 724 } 725 726 /** 727 * Provides serialization support. 728 * 729 * @param stream the input stream. 730 * 731 * @throws IOException if there is an I/O error. 732 * @throws ClassNotFoundException if there is a classpath problem. 733 */ 734 private void readObject(ObjectInputStream stream) 735 throws IOException, ClassNotFoundException { 736 stream.defaultReadObject(); 737 for (Object item : this.data) { 738 TaskSeries series = (TaskSeries) item; 739 series.addChangeListener(this); 740 } 741 } 742}