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 * KeyedObject2D.java
029 * ------------------
030 * (C) Copyright 2003-2022, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.data;
038
039import java.io.Serializable;
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.List;
043import org.jfree.chart.internal.Args;
044
045/**
046 * A data structure that stores zero, one or many objects, where each object is
047 * associated with two keys (a 'row' key and a 'column' key).
048 */
049public class KeyedObjects2D<R extends Comparable<R>, C extends Comparable<C>> 
050        implements Cloneable, Serializable {
051
052    /** For serialization. */
053    private static final long serialVersionUID = -1015873563138522374L;
054
055    /** The row keys. */
056    private List<R> rowKeys;
057
058    /** The column keys. */
059    private List<C> columnKeys;
060
061    /** The row data. */
062    private List<KeyedObjects> rows;
063
064    /**
065     * Creates a new instance (initially empty).
066     */
067    public KeyedObjects2D() {
068        this.rowKeys = new ArrayList<>();
069        this.columnKeys = new ArrayList<>();
070        this.rows = new java.util.ArrayList();
071    }
072
073    /**
074     * Returns the row count.
075     *
076     * @return The row count.
077     *
078     * @see #getColumnCount()
079     */
080    public int getRowCount() {
081        return this.rowKeys.size();
082    }
083
084    /**
085     * Returns the column count.
086     *
087     * @return The column count.
088     *
089     * @see #getRowCount()
090     */
091    public int getColumnCount() {
092        return this.columnKeys.size();
093    }
094
095    /**
096     * Returns the object for a given row and column.
097     *
098     * @param row  the row index (in the range 0 to getRowCount() - 1).
099     * @param column  the column index (in the range 0 to getColumnCount() - 1).
100     *
101     * @return The object (possibly {@code null}).
102     *
103     * @see #getObject(Comparable, Comparable)
104     */
105    public Object getObject(int row, int column) {
106        Object result = null;
107        KeyedObjects rowData = (KeyedObjects) this.rows.get(row);
108        if (rowData != null) {
109            Comparable columnKey = (Comparable) this.columnKeys.get(column);
110            if (columnKey != null) {
111                int index = rowData.getIndex(columnKey);
112                if (index >= 0) {
113                    result = rowData.getObject(columnKey);
114                }
115            }
116        }
117        return result;
118    }
119
120    /**
121     * Returns the key for a given row.
122     *
123     * @param row  the row index (zero based).
124     *
125     * @return The row index.
126     *
127     * @see #getRowIndex(Comparable)
128     */
129    public R getRowKey(int row) {
130        return this.rowKeys.get(row);
131    }
132
133    /**
134     * Returns the row index for a given key, or {@code -1} if the key
135     * is not recognised.
136     *
137     * @param key  the key ({@code null} not permitted).
138     *
139     * @return The row index.
140     *
141     * @see #getRowKey(int)
142     */
143    public int getRowIndex(R key) {
144        Args.nullNotPermitted(key, "key");
145        return this.rowKeys.indexOf(key);
146    }
147
148    /**
149     * Returns the row keys.
150     *
151     * @return The row keys (never {@code null}).
152     *
153     * @see #getRowKeys()
154     */
155    public List<R> getRowKeys() {
156        return Collections.unmodifiableList(this.rowKeys);
157    }
158
159    /**
160     * Returns the key for a given column.
161     *
162     * @param column  the column.
163     *
164     * @return The key.
165     *
166     * @see #getColumnIndex(Comparable)
167     */
168    public C getColumnKey(int column) {
169        return this.columnKeys.get(column);
170    }
171
172    /**
173     * Returns the column index for a given key, or {@code -1} if the key
174     * is not recognised.
175     *
176     * @param key  the key ({@code null} not permitted).
177     *
178     * @return The column index.
179     *
180     * @see #getColumnKey(int)
181     */
182    public int getColumnIndex(C key) {
183        Args.nullNotPermitted(key, "key");
184        return this.columnKeys.indexOf(key);
185    }
186
187    /**
188     * Returns the column keys.
189     *
190     * @return The column keys (never {@code null}).
191     *
192     * @see #getRowKeys()
193     */
194    public List<C> getColumnKeys() {
195        return Collections.unmodifiableList(this.columnKeys);
196    }
197
198    /**
199     * Returns the object for the given row and column keys.
200     *
201     * @param rowKey  the row key ({@code null} not permitted).
202     * @param columnKey  the column key ({@code null} not permitted).
203     *
204     * @return The object (possibly {@code null}).
205     *
206     * @throws IllegalArgumentException if {@code rowKey} or
207     *         {@code columnKey} is {@code null}.
208     * @throws UnknownKeyException if {@code rowKey} or
209     *         {@code columnKey} is not recognised.
210     */
211    public Object getObject(R rowKey, C columnKey) {
212        Args.nullNotPermitted(rowKey, "rowKey");
213        Args.nullNotPermitted(columnKey, "columnKey");
214        int row = this.rowKeys.indexOf(rowKey);
215        if (row < 0) {
216            throw new UnknownKeyException("Row key (" + rowKey
217                    + ") not recognised.");
218        }
219        int column = this.columnKeys.indexOf(columnKey);
220        if (column < 0) {
221            throw new UnknownKeyException("Column key (" + columnKey
222                    + ") not recognised.");
223        }
224        KeyedObjects rowData = (KeyedObjects) this.rows.get(row);
225        int index = rowData.getIndex(columnKey);
226        if (index >= 0) {
227            return rowData.getObject(index);
228        }
229        else {
230            return null;
231        }
232    }
233
234    /**
235     * Adds an object to the table.  Performs the same function as setObject().
236     *
237     * @param object  the object.
238     * @param rowKey  the row key ({@code null} not permitted).
239     * @param columnKey  the column key ({@code null} not permitted).
240     */
241    public void addObject(Object object, R rowKey, C columnKey) {
242        setObject(object, rowKey, columnKey);
243    }
244
245    /**
246     * Adds or updates an object.
247     *
248     * @param object  the object.
249     * @param rowKey  the row key ({@code null} not permitted).
250     * @param columnKey  the column key ({@code null} not permitted).
251     */
252    public void setObject(Object object, R rowKey, C columnKey) {
253        Args.nullNotPermitted(rowKey, "rowKey");
254        Args.nullNotPermitted(columnKey, "columnKey");
255        KeyedObjects row;
256        int rowIndex = this.rowKeys.indexOf(rowKey);
257        if (rowIndex >= 0) {
258            row = (KeyedObjects) this.rows.get(rowIndex);
259        }
260        else {
261            this.rowKeys.add(rowKey);
262            row = new KeyedObjects();
263            this.rows.add(row);
264        }
265        row.setObject(columnKey, object);
266        int columnIndex = this.columnKeys.indexOf(columnKey);
267        if (columnIndex < 0) {
268            this.columnKeys.add(columnKey);
269        }
270    }
271
272    /**
273     * Removes an object from the table by setting it to {@code null}.  If
274     * all the objects in the specified row and/or column are now
275     * {@code null}, the row and/or column is removed from the table.
276     *
277     * @param rowKey  the row key ({@code null} not permitted).
278     * @param columnKey  the column key ({@code null} not permitted).
279     *
280     * @see #addObject(Object, Comparable, Comparable)
281     */
282    public void removeObject(R rowKey, C columnKey) {
283        int rowIndex = getRowIndex(rowKey);
284        if (rowIndex < 0) {
285            throw new UnknownKeyException("Row key (" + rowKey
286                    + ") not recognised.");
287        }
288        int columnIndex = getColumnIndex(columnKey);
289        if (columnIndex < 0) {
290            throw new UnknownKeyException("Column key (" + columnKey
291                    + ") not recognised.");
292        }
293        setObject(null, rowKey, columnKey);
294
295        // 1. check whether the row is now empty.
296        boolean allNull = true;
297        KeyedObjects row = (KeyedObjects) this.rows.get(rowIndex);
298
299        for (int item = 0, itemCount = row.getItemCount(); item < itemCount;
300             item++) {
301            if (row.getObject(item) != null) {
302                allNull = false;
303                break;
304            }
305        }
306
307        if (allNull) {
308            this.rowKeys.remove(rowIndex);
309            this.rows.remove(rowIndex);
310        }
311
312        // 2. check whether the column is now empty.
313        allNull = true;
314
315        for (int item = 0, itemCount = this.rows.size(); item < itemCount;
316             item++) {
317            row = (KeyedObjects) this.rows.get(item);
318            int colIndex = row.getIndex(columnKey);
319            if (colIndex >= 0 && row.getObject(colIndex) != null) {
320                allNull = false;
321                break;
322            }
323        }
324
325        if (allNull) {
326            for (int item = 0, itemCount = this.rows.size(); item < itemCount;
327                 item++) {
328                row = (KeyedObjects) this.rows.get(item);
329                int colIndex = row.getIndex(columnKey);
330                if (colIndex >= 0) {
331                    row.removeValue(colIndex);
332                }
333            }
334            this.columnKeys.remove(columnKey);
335        }
336    }
337
338    /**
339     * Removes an entire row from the table.
340     *
341     * @param rowIndex  the row index.
342     *
343     * @see #removeColumn(int)
344     */
345    public void removeRow(int rowIndex) {
346        this.rowKeys.remove(rowIndex);
347        this.rows.remove(rowIndex);
348    }
349
350    /**
351     * Removes an entire row from the table.
352     *
353     * @param rowKey  the row key ({@code null} not permitted).
354     *
355     * @throws UnknownKeyException if {@code rowKey} is not recognised.
356     *
357     * @see #removeColumn(Comparable)
358     */
359    public void removeRow(R rowKey) {
360        int index = getRowIndex(rowKey);
361        if (index < 0) {
362            throw new UnknownKeyException("Row key (" + rowKey
363                    + ") not recognised.");
364        }
365        removeRow(index);
366    }
367
368    /**
369     * Removes an entire column from the table.
370     *
371     * @param columnIndex  the column index.
372     *
373     * @see #removeRow(int)
374     */
375    public void removeColumn(int columnIndex) {
376        C columnKey = getColumnKey(columnIndex);
377        removeColumn(columnKey);
378    }
379
380    /**
381     * Removes an entire column from the table.
382     *
383     * @param columnKey  the column key ({@code null} not permitted).
384     *
385     * @throws UnknownKeyException if {@code rowKey} is not recognised.
386     *
387     * @see #removeRow(Comparable)
388     */
389    public void removeColumn(C columnKey) {
390        int index = getColumnIndex(columnKey);
391        if (index < 0) {
392            throw new UnknownKeyException("Column key (" + columnKey
393                    + ") not recognised.");
394        }
395        for (KeyedObjects rowData : this.rows) {
396            int i = rowData.getIndex(columnKey);
397            if (i >= 0) {
398                rowData.removeValue(i);
399            }
400        }
401        this.columnKeys.remove(columnKey);
402    }
403
404    /**
405     * Clears all the data and associated keys.
406     *
407     * @since 1.0.7
408     */
409    public void clear() {
410        this.rowKeys.clear();
411        this.columnKeys.clear();
412        this.rows.clear();
413    }
414
415    /**
416     * Tests this object for equality with an arbitrary object.
417     *
418     * @param obj  the object to test ({@code null} permitted).
419     *
420     * @return A boolean.
421     */
422    @Override
423    public boolean equals(Object obj) {
424        if (obj == this) {
425            return true;
426        }
427        if (!(obj instanceof KeyedObjects2D)) {
428            return false;
429        }
430
431        KeyedObjects2D that = (KeyedObjects2D) obj;
432        if (!getRowKeys().equals(that.getRowKeys())) {
433            return false;
434        }
435        if (!getColumnKeys().equals(that.getColumnKeys())) {
436            return false;
437        }
438        int rowCount = getRowCount();
439        if (rowCount != that.getRowCount()) {
440            return false;
441        }
442        int colCount = getColumnCount();
443        if (colCount != that.getColumnCount()) {
444            return false;
445        }
446        for (int r = 0; r < rowCount; r++) {
447            for (int c = 0; c < colCount; c++) {
448                Object v1 = getObject(r, c);
449                Object v2 = that.getObject(r, c);
450                if (v1 == null) {
451                    if (v2 != null) {
452                        return false;
453                    }
454                }
455                else {
456                    if (!v1.equals(v2)) {
457                        return false;
458                    }
459                }
460            }
461        }
462        return true;
463    }
464
465    /**
466     * Returns a hashcode for this object.
467     *
468     * @return A hashcode.
469     */
470    @Override
471    public int hashCode() {
472        int result;
473        result = this.rowKeys.hashCode();
474        result = 29 * result + this.columnKeys.hashCode();
475        result = 29 * result + this.rows.hashCode();
476        return result;
477    }
478
479    /**
480     * Returns a clone.
481     *
482     * @return A clone.
483     *
484     * @throws CloneNotSupportedException  this class will not throw this
485     *         exception, but subclasses (if any) might.
486     */
487    @Override
488    public Object clone() throws CloneNotSupportedException {
489        KeyedObjects2D clone = (KeyedObjects2D) super.clone();
490        clone.columnKeys = new java.util.ArrayList(this.columnKeys);
491        clone.rowKeys = new java.util.ArrayList(this.rowKeys);
492        clone.rows = new java.util.ArrayList(this.rows.size());
493        for (KeyedObjects row : this.rows) {
494            clone.rows.add(row.clone());
495        }
496        return clone;
497    }
498
499}