001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * -----------------------------------
028 * DefaultIntervalCategoryDataset.java
029 * -----------------------------------
030 * (C) Copyright 2002-2008, by Jeremy Bowman and Contributors.
031 *
032 * Original Author: Jeremy Bowman;
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 *
035 * Changes
036 * -------
037 * 29-Apr-2002 : Version 1, contributed by Jeremy Bowman (DG);
038 * 24-Oct-2002 : Amendments for changes made to the dataset interface (DG);
039 * ------------- JFREECHART 1.0.x ---------------------------------------------
040 * 08-Mar-2007 : Added equals() and clone() overrides (DG);
041 * 25-Feb-2008 : Fix for the special case where the dataset is empty, see bug
042 * 1897580 (DG)
043 *
044 */
045
046 package org.jfree.data.category;
047
048 import java.util.ArrayList;
049 import java.util.Arrays;
050 import java.util.Collections;
051 import java.util.List;
052 import java.util.ResourceBundle;
053
054 import org.jfree.data.DataUtilities;
055 import org.jfree.data.UnknownKeyException;
056 import org.jfree.data.general.AbstractSeriesDataset;
057
058 /**
059 * A convenience class that provides a default implementation of the
060 * {@link IntervalCategoryDataset} interface.
061 * <p>
062 * The standard constructor accepts data in a two dimensional array where the
063 * first dimension is the series, and the second dimension is the category.
064 */
065 public class DefaultIntervalCategoryDataset extends AbstractSeriesDataset
066 implements IntervalCategoryDataset {
067
068 /** The series keys. */
069 private Comparable[] seriesKeys;
070
071 /** The category keys. */
072 private Comparable[] categoryKeys;
073
074 /** Storage for the start value data. */
075 private Number[][] startData;
076
077 /** Storage for the end value data. */
078 private Number[][] endData;
079
080 /**
081 * Creates a new dataset using the specified data values and automatically
082 * generated series and category keys.
083 *
084 * @param starts the starting values for the intervals (<code>null</code>
085 * not permitted).
086 * @param ends the ending values for the intervals (<code>null</code> not
087 * permitted).
088 */
089 public DefaultIntervalCategoryDataset(double[][] starts, double[][] ends) {
090 this(DataUtilities.createNumberArray2D(starts),
091 DataUtilities.createNumberArray2D(ends));
092 }
093
094 /**
095 * Constructs a dataset and populates it with data from the array.
096 * <p>
097 * The arrays are indexed as data[series][category]. Series and category
098 * names are automatically generated - you can change them using the
099 * {@link #setSeriesKeys(Comparable[])} and
100 * {@link #setCategoryKeys(Comparable[])} methods.
101 *
102 * @param starts the start values data.
103 * @param ends the end values data.
104 */
105 public DefaultIntervalCategoryDataset(Number[][] starts, Number[][] ends) {
106 this(null, null, starts, ends);
107 }
108
109 /**
110 * Constructs a DefaultIntervalCategoryDataset, populates it with data
111 * from the arrays, and uses the supplied names for the series.
112 * <p>
113 * Category names are generated automatically ("Category 1", "Category 2",
114 * etc).
115 *
116 * @param seriesNames the series names (if <code>null</code>, series names
117 * will be generated automatically).
118 * @param starts the start values data, indexed as data[series][category].
119 * @param ends the end values data, indexed as data[series][category].
120 */
121 public DefaultIntervalCategoryDataset(String[] seriesNames,
122 Number[][] starts,
123 Number[][] ends) {
124
125 this(seriesNames, null, starts, ends);
126
127 }
128
129 /**
130 * Constructs a DefaultIntervalCategoryDataset, populates it with data
131 * from the arrays, and uses the supplied names for the series and the
132 * supplied objects for the categories.
133 *
134 * @param seriesKeys the series keys (if <code>null</code>, series keys
135 * will be generated automatically).
136 * @param categoryKeys the category keys (if <code>null</code>, category
137 * keys will be generated automatically).
138 * @param starts the start values data, indexed as data[series][category].
139 * @param ends the end values data, indexed as data[series][category].
140 */
141 public DefaultIntervalCategoryDataset(Comparable[] seriesKeys,
142 Comparable[] categoryKeys,
143 Number[][] starts,
144 Number[][] ends) {
145
146 this.startData = starts;
147 this.endData = ends;
148
149 if (starts != null && ends != null) {
150
151 String baseName = "org.jfree.data.resources.DataPackageResources";
152 ResourceBundle resources = ResourceBundle.getBundle(baseName);
153
154 int seriesCount = starts.length;
155 if (seriesCount != ends.length) {
156 String errMsg = "DefaultIntervalCategoryDataset: the number "
157 + "of series in the start value dataset does "
158 + "not match the number of series in the end "
159 + "value dataset.";
160 throw new IllegalArgumentException(errMsg);
161 }
162 if (seriesCount > 0) {
163
164 // set up the series names...
165 if (seriesKeys != null) {
166
167 if (seriesKeys.length != seriesCount) {
168 throw new IllegalArgumentException(
169 "The number of series keys does not "
170 + "match the number of series in the data.");
171 }
172
173 this.seriesKeys = seriesKeys;
174 }
175 else {
176 String prefix = resources.getString(
177 "series.default-prefix") + " ";
178 this.seriesKeys = generateKeys(seriesCount, prefix);
179 }
180
181 // set up the category names...
182 int categoryCount = starts[0].length;
183 if (categoryCount != ends[0].length) {
184 String errMsg = "DefaultIntervalCategoryDataset: the "
185 + "number of categories in the start value "
186 + "dataset does not match the number of "
187 + "categories in the end value dataset.";
188 throw new IllegalArgumentException(errMsg);
189 }
190 if (categoryKeys != null) {
191 if (categoryKeys.length != categoryCount) {
192 throw new IllegalArgumentException(
193 "The number of category keys does not match "
194 + "the number of categories in the data.");
195 }
196 this.categoryKeys = categoryKeys;
197 }
198 else {
199 String prefix = resources.getString(
200 "categories.default-prefix") + " ";
201 this.categoryKeys = generateKeys(categoryCount, prefix);
202 }
203
204 }
205 else {
206 this.seriesKeys = new Comparable[0];
207 this.categoryKeys = new Comparable[0];
208 }
209 }
210
211 }
212
213 /**
214 * Returns the number of series in the dataset (possibly zero).
215 *
216 * @return The number of series in the dataset.
217 *
218 * @see #getRowCount()
219 * @see #getCategoryCount()
220 */
221 public int getSeriesCount() {
222 int result = 0;
223 if (this.startData != null) {
224 result = this.startData.length;
225 }
226 return result;
227 }
228
229 /**
230 * Returns a series index.
231 *
232 * @param seriesKey the series key.
233 *
234 * @return The series index.
235 *
236 * @see #getRowIndex(Comparable)
237 * @see #getSeriesKey(int)
238 */
239 public int getSeriesIndex(Comparable seriesKey) {
240 int result = -1;
241 for (int i = 0; i < this.seriesKeys.length; i++) {
242 if (seriesKey.equals(this.seriesKeys[i])) {
243 result = i;
244 break;
245 }
246 }
247 return result;
248 }
249
250 /**
251 * Returns the name of the specified series.
252 *
253 * @param series the index of the required series (zero-based).
254 *
255 * @return The name of the specified series.
256 *
257 * @see #getSeriesIndex(Comparable)
258 */
259 public Comparable getSeriesKey(int series) {
260 if ((series >= getSeriesCount()) || (series < 0)) {
261 throw new IllegalArgumentException("No such series : " + series);
262 }
263 return this.seriesKeys[series];
264 }
265
266 /**
267 * Sets the names of the series in the dataset.
268 *
269 * @param seriesKeys the new keys (<code>null</code> not permitted, the
270 * length of the array must match the number of series in the
271 * dataset).
272 *
273 * @see #setCategoryKeys(Comparable[])
274 */
275 public void setSeriesKeys(Comparable[] seriesKeys) {
276 if (seriesKeys == null) {
277 throw new IllegalArgumentException("Null 'seriesKeys' argument.");
278 }
279 if (seriesKeys.length != getSeriesCount()) {
280 throw new IllegalArgumentException(
281 "The number of series keys does not match the data.");
282 }
283 this.seriesKeys = seriesKeys;
284 fireDatasetChanged();
285 }
286
287 /**
288 * Returns the number of categories in the dataset.
289 *
290 * @return The number of categories in the dataset.
291 *
292 * @see #getColumnCount()
293 */
294 public int getCategoryCount() {
295 int result = 0;
296 if (this.startData != null) {
297 if (getSeriesCount() > 0) {
298 result = this.startData[0].length;
299 }
300 }
301 return result;
302 }
303
304 /**
305 * Returns a list of the categories in the dataset. This method supports
306 * the {@link CategoryDataset} interface.
307 *
308 * @return A list of the categories in the dataset.
309 *
310 * @see #getRowKeys()
311 */
312 public List getColumnKeys() {
313 // the CategoryDataset interface expects a list of categories, but
314 // we've stored them in an array...
315 if (this.categoryKeys == null) {
316 return new ArrayList();
317 }
318 else {
319 return Collections.unmodifiableList(Arrays.asList(
320 this.categoryKeys));
321 }
322 }
323
324 /**
325 * Sets the categories for the dataset.
326 *
327 * @param categoryKeys an array of objects representing the categories in
328 * the dataset.
329 *
330 * @see #getRowKeys()
331 * @see #setSeriesKeys(Comparable[])
332 */
333 public void setCategoryKeys(Comparable[] categoryKeys) {
334 if (categoryKeys == null) {
335 throw new IllegalArgumentException("Null 'categoryKeys' argument.");
336 }
337 if (categoryKeys.length != getCategoryCount()) {
338 throw new IllegalArgumentException(
339 "The number of categories does not match the data.");
340 }
341 for (int i = 0; i < categoryKeys.length; i++) {
342 if (categoryKeys[i] == null) {
343 throw new IllegalArgumentException(
344 "DefaultIntervalCategoryDataset.setCategoryKeys(): "
345 + "null category not permitted.");
346 }
347 }
348 this.categoryKeys = categoryKeys;
349 fireDatasetChanged();
350 }
351
352 /**
353 * Returns the data value for one category in a series.
354 * <P>
355 * This method is part of the CategoryDataset interface. Not particularly
356 * meaningful for this class...returns the end value.
357 *
358 * @param series The required series (zero based index).
359 * @param category The required category.
360 *
361 * @return The data value for one category in a series (null possible).
362 *
363 * @see #getEndValue(Comparable, Comparable)
364 */
365 public Number getValue(Comparable series, Comparable category) {
366 int seriesIndex = getSeriesIndex(series);
367 if (seriesIndex < 0) {
368 throw new UnknownKeyException("Unknown 'series' key.");
369 }
370 int itemIndex = getColumnIndex(category);
371 if (itemIndex < 0) {
372 throw new UnknownKeyException("Unknown 'category' key.");
373 }
374 return getValue(seriesIndex, itemIndex);
375 }
376
377 /**
378 * Returns the data value for one category in a series.
379 * <P>
380 * This method is part of the CategoryDataset interface. Not particularly
381 * meaningful for this class...returns the end value.
382 *
383 * @param series the required series (zero based index).
384 * @param category the required category.
385 *
386 * @return The data value for one category in a series (null possible).
387 *
388 * @see #getEndValue(int, int)
389 */
390 public Number getValue(int series, int category) {
391 return getEndValue(series, category);
392 }
393
394 /**
395 * Returns the start data value for one category in a series.
396 *
397 * @param series the required series.
398 * @param category the required category.
399 *
400 * @return The start data value for one category in a series
401 * (possibly <code>null</code>).
402 *
403 * @see #getStartValue(int, int)
404 */
405 public Number getStartValue(Comparable series, Comparable category) {
406 int seriesIndex = getSeriesIndex(series);
407 if (seriesIndex < 0) {
408 throw new UnknownKeyException("Unknown 'series' key.");
409 }
410 int itemIndex = getColumnIndex(category);
411 if (itemIndex < 0) {
412 throw new UnknownKeyException("Unknown 'category' key.");
413 }
414 return getStartValue(seriesIndex, itemIndex);
415 }
416
417 /**
418 * Returns the start data value for one category in a series.
419 *
420 * @param series the required series (zero based index).
421 * @param category the required category.
422 *
423 * @return The start data value for one category in a series
424 * (possibly <code>null</code>).
425 *
426 * @see #getStartValue(Comparable, Comparable)
427 */
428 public Number getStartValue(int series, int category) {
429
430 // check arguments...
431 if ((series < 0) || (series >= getSeriesCount())) {
432 throw new IllegalArgumentException(
433 "DefaultIntervalCategoryDataset.getValue(): "
434 + "series index out of range.");
435 }
436
437 if ((category < 0) || (category >= getCategoryCount())) {
438 throw new IllegalArgumentException(
439 "DefaultIntervalCategoryDataset.getValue(): "
440 + "category index out of range.");
441 }
442
443 // fetch the value...
444 return this.startData[series][category];
445
446 }
447
448 /**
449 * Returns the end data value for one category in a series.
450 *
451 * @param series the required series.
452 * @param category the required category.
453 *
454 * @return The end data value for one category in a series (null possible).
455 *
456 * @see #getEndValue(int, int)
457 */
458 public Number getEndValue(Comparable series, Comparable category) {
459 int seriesIndex = getSeriesIndex(series);
460 if (seriesIndex < 0) {
461 throw new UnknownKeyException("Unknown 'series' key.");
462 }
463 int itemIndex = getColumnIndex(category);
464 if (itemIndex < 0) {
465 throw new UnknownKeyException("Unknown 'category' key.");
466 }
467 return getEndValue(seriesIndex, itemIndex);
468 }
469
470 /**
471 * Returns the end data value for one category in a series.
472 *
473 * @param series the required series (zero based index).
474 * @param category the required category.
475 *
476 * @return The end data value for one category in a series (null possible).
477 *
478 * @see #getEndValue(Comparable, Comparable)
479 */
480 public Number getEndValue(int series, int category) {
481 if ((series < 0) || (series >= getSeriesCount())) {
482 throw new IllegalArgumentException(
483 "DefaultIntervalCategoryDataset.getValue(): "
484 + "series index out of range.");
485 }
486
487 if ((category < 0) || (category >= getCategoryCount())) {
488 throw new IllegalArgumentException(
489 "DefaultIntervalCategoryDataset.getValue(): "
490 + "category index out of range.");
491 }
492
493 return this.endData[series][category];
494 }
495
496 /**
497 * Sets the start data value for one category in a series.
498 *
499 * @param series the series (zero-based index).
500 * @param category the category.
501 *
502 * @param value The value.
503 *
504 * @see #setEndValue(int, Comparable, Number)
505 */
506 public void setStartValue(int series, Comparable category, Number value) {
507
508 // does the series exist?
509 if ((series < 0) || (series > getSeriesCount() - 1)) {
510 throw new IllegalArgumentException(
511 "DefaultIntervalCategoryDataset.setValue: "
512 + "series outside valid range.");
513 }
514
515 // is the category valid?
516 int categoryIndex = getCategoryIndex(category);
517 if (categoryIndex < 0) {
518 throw new IllegalArgumentException(
519 "DefaultIntervalCategoryDataset.setValue: "
520 + "unrecognised category.");
521 }
522
523 // update the data...
524 this.startData[series][categoryIndex] = value;
525 fireDatasetChanged();
526
527 }
528
529 /**
530 * Sets the end data value for one category in a series.
531 *
532 * @param series the series (zero-based index).
533 * @param category the category.
534 *
535 * @param value the value.
536 *
537 * @see #setStartValue(int, Comparable, Number)
538 */
539 public void setEndValue(int series, Comparable category, Number value) {
540
541 // does the series exist?
542 if ((series < 0) || (series > getSeriesCount() - 1)) {
543 throw new IllegalArgumentException(
544 "DefaultIntervalCategoryDataset.setValue: "
545 + "series outside valid range.");
546 }
547
548 // is the category valid?
549 int categoryIndex = getCategoryIndex(category);
550 if (categoryIndex < 0) {
551 throw new IllegalArgumentException(
552 "DefaultIntervalCategoryDataset.setValue: "
553 + "unrecognised category.");
554 }
555
556 // update the data...
557 this.endData[series][categoryIndex] = value;
558 fireDatasetChanged();
559
560 }
561
562 /**
563 * Returns the index for the given category.
564 *
565 * @param category the category (<code>null</code> not permitted).
566 *
567 * @return The index.
568 *
569 * @see #getColumnIndex(Comparable)
570 */
571 public int getCategoryIndex(Comparable category) {
572 int result = -1;
573 for (int i = 0; i < this.categoryKeys.length; i++) {
574 if (category.equals(this.categoryKeys[i])) {
575 result = i;
576 break;
577 }
578 }
579 return result;
580 }
581
582 /**
583 * Generates an array of keys, by appending a space plus an integer
584 * (starting with 1) to the supplied prefix string.
585 *
586 * @param count the number of keys required.
587 * @param prefix the name prefix.
588 *
589 * @return An array of <i>prefixN</i> with N = { 1 .. count}.
590 */
591 private Comparable[] generateKeys(int count, String prefix) {
592 Comparable[] result = new Comparable[count];
593 String name;
594 for (int i = 0; i < count; i++) {
595 name = prefix + (i + 1);
596 result[i] = name;
597 }
598 return result;
599 }
600
601 /**
602 * Returns a column key.
603 *
604 * @param column the column index.
605 *
606 * @return The column key.
607 *
608 * @see #getRowKey(int)
609 */
610 public Comparable getColumnKey(int column) {
611 return this.categoryKeys[column];
612 }
613
614 /**
615 * Returns a column index.
616 *
617 * @param columnKey the column key (<code>null</code> not permitted).
618 *
619 * @return The column index.
620 *
621 * @see #getCategoryIndex(Comparable)
622 */
623 public int getColumnIndex(Comparable columnKey) {
624 if (columnKey == null) {
625 throw new IllegalArgumentException("Null 'columnKey' argument.");
626 }
627 return getCategoryIndex(columnKey);
628 }
629
630 /**
631 * Returns a row index.
632 *
633 * @param rowKey the row key.
634 *
635 * @return The row index.
636 *
637 * @see #getSeriesIndex(Comparable)
638 */
639 public int getRowIndex(Comparable rowKey) {
640 return getSeriesIndex(rowKey);
641 }
642
643 /**
644 * Returns a list of the series in the dataset. This method supports the
645 * {@link CategoryDataset} interface.
646 *
647 * @return A list of the series in the dataset.
648 *
649 * @see #getColumnKeys()
650 */
651 public List getRowKeys() {
652 // the CategoryDataset interface expects a list of series, but
653 // we've stored them in an array...
654 if (this.seriesKeys == null) {
655 return new java.util.ArrayList();
656 }
657 else {
658 return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
659 }
660 }
661
662 /**
663 * Returns the name of the specified series.
664 *
665 * @param row the index of the required row/series (zero-based).
666 *
667 * @return The name of the specified series.
668 *
669 * @see #getColumnKey(int)
670 */
671 public Comparable getRowKey(int row) {
672 if ((row >= getRowCount()) || (row < 0)) {
673 throw new IllegalArgumentException(
674 "The 'row' argument is out of bounds.");
675 }
676 return this.seriesKeys[row];
677 }
678
679 /**
680 * Returns the number of categories in the dataset. This method is part of
681 * the {@link CategoryDataset} interface.
682 *
683 * @return The number of categories in the dataset.
684 *
685 * @see #getCategoryCount()
686 * @see #getRowCount()
687 */
688 public int getColumnCount() {
689 return this.categoryKeys.length;
690 }
691
692 /**
693 * Returns the number of series in the dataset (possibly zero).
694 *
695 * @return The number of series in the dataset.
696 *
697 * @see #getSeriesCount()
698 * @see #getColumnCount()
699 */
700 public int getRowCount() {
701 return this.seriesKeys.length;
702 }
703
704 /**
705 * Tests this dataset for equality with an arbitrary object.
706 *
707 * @param obj the object (<code>null</code> permitted).
708 *
709 * @return A boolean.
710 */
711 public boolean equals(Object obj) {
712 if (obj == this) {
713 return true;
714 }
715 if (!(obj instanceof DefaultIntervalCategoryDataset)) {
716 return false;
717 }
718 DefaultIntervalCategoryDataset that
719 = (DefaultIntervalCategoryDataset) obj;
720 if (!Arrays.equals(this.seriesKeys, that.seriesKeys)) {
721 return false;
722 }
723 if (!Arrays.equals(this.categoryKeys, that.categoryKeys)) {
724 return false;
725 }
726 if (!equal(this.startData, that.startData)) {
727 return false;
728 }
729 if (!equal(this.endData, that.endData)) {
730 return false;
731 }
732 // seem to be the same...
733 return true;
734 }
735
736 /**
737 * Returns a clone of this dataset.
738 *
739 * @return A clone.
740 *
741 * @throws CloneNotSupportedException if there is a problem cloning the
742 * dataset.
743 */
744 public Object clone() throws CloneNotSupportedException {
745 DefaultIntervalCategoryDataset clone
746 = (DefaultIntervalCategoryDataset) super.clone();
747 clone.categoryKeys = (Comparable[]) this.categoryKeys.clone();
748 clone.seriesKeys = (Comparable[]) this.seriesKeys.clone();
749 clone.startData = clone(this.startData);
750 clone.endData = clone(this.endData);
751 return clone;
752 }
753
754 /**
755 * Tests two double[][] arrays for equality.
756 *
757 * @param array1 the first array (<code>null</code> permitted).
758 * @param array2 the second arrray (<code>null</code> permitted).
759 *
760 * @return A boolean.
761 */
762 private static boolean equal(Number[][] array1, Number[][] array2) {
763 if (array1 == null) {
764 return (array2 == null);
765 }
766 if (array2 == null) {
767 return false;
768 }
769 if (array1.length != array2.length) {
770 return false;
771 }
772 for (int i = 0; i < array1.length; i++) {
773 if (!Arrays.equals(array1[i], array2[i])) {
774 return false;
775 }
776 }
777 return true;
778 }
779
780 /**
781 * Clones a two dimensional array of <code>Number</code> objects.
782 *
783 * @param array the array (<code>null</code> not permitted).
784 *
785 * @return A clone of the array.
786 */
787 private static Number[][] clone(Number[][] array) {
788 if (array == null) {
789 throw new IllegalArgumentException("Null 'array' argument.");
790 }
791 Number[][] result = new Number[array.length][];
792 for (int i = 0; i < array.length; i++) {
793 Number[] child = array[i];
794 Number[] copychild = new Number[child.length];
795 System.arraycopy(child, 0, copychild, 0, child.length);
796 result[i] = copychild;
797 }
798 return result;
799 }
800
801 /**
802 * Returns a list of the series in the dataset.
803 *
804 * @return A list of the series in the dataset.
805 *
806 * @deprecated Use {@link #getRowKeys()} instead.
807 */
808 public List getSeries() {
809 if (this.seriesKeys == null) {
810 return new java.util.ArrayList();
811 }
812 else {
813 return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
814 }
815 }
816
817 /**
818 * Returns a list of the categories in the dataset.
819 *
820 * @return A list of the categories in the dataset.
821 *
822 * @deprecated Use {@link #getColumnKeys()} instead.
823 */
824 public List getCategories() {
825 return getColumnKeys();
826 }
827
828 /**
829 * Returns the item count.
830 *
831 * @return The item count.
832 *
833 * @deprecated Use {@link #getCategoryCount()} instead.
834 */
835 public int getItemCount() {
836 return this.categoryKeys.length;
837 }
838
839 }