001package ball.util;
002/*-
003 * ##########################################################################
004 * Utilities
005 * %%
006 * Copyright (C) 2008 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import java.lang.reflect.ParameterizedType;
022import java.lang.reflect.Type;
023import java.util.AbstractList;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Comparator;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.SortedMap;
031import java.util.TreeMap;
032import javax.swing.event.EventListenerList;
033import javax.swing.event.TableModelEvent;
034import javax.swing.event.TableModelListener;
035import javax.swing.table.TableModel;
036
037/**
038 * {@link Coordinate} {@link java.util.Map} implementation.
039 *
040 * @param       <V>             The value type.
041 *
042 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
043 */
044public class CoordinateMap<V> extends MapView<Coordinate,V> implements SortedMap<Coordinate,V>, TableModel {
045    private static final long serialVersionUID = -7283339807212986103L;
046
047    /** @serial */ private Coordinate min = null;
048    /** @serial */ private Coordinate max = null;
049    /** @serial */ private final EventListenerList list = new EventListenerList();
050
051    /**
052     * Constructor to create an empty {@link CoordinateMap}.
053     */
054    public CoordinateMap() { super(new TreeMap<>()); }
055
056    /**
057     * Constructor to specify minimum and maximum {@code Y} and {@code X}.
058     *
059     * @param   y0              {@code MIN(y)}
060     * @param   x0              {@code MIN(x)}
061     * @param   yN              {@code MAX(y) + 1}
062     * @param   xN              {@code MAX(x) + 1}
063     */
064    public CoordinateMap(Number y0, Number x0, Number yN, Number xN) {
065        this();
066
067        resize(y0, x0, yN, xN);
068    }
069
070    /**
071     * Constructor to specify maximum {@code Y} and {@code X} (origin
072     * {@code (0, 0)}).
073     *
074     * @param   yN              {@code MAX(y) + 1}
075     * @param   xN              {@code MAX(x) + 1}
076     */
077    public CoordinateMap(Number yN, Number xN) { this(0, 0, yN, xN); }
078
079    private CoordinateMap(Map<Coordinate,V> map, Number y0, Number x0, Number yN, Number xN) {
080        super(map);
081
082        resize(y0, x0, yN, xN);
083    }
084
085    @Override
086    protected SortedMap<Coordinate,V> map() {
087        return (SortedMap<Coordinate,V>) super.map();
088    }
089
090    /**
091     * Method to get the value type of {@link.this} {@link CoordinateMap}.
092     *
093     * @return  The value type {@link Class}.
094     */
095    protected Type getType() {
096        return ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
097    }
098
099    /**
100     * Method to specify new limits for the {@link CoordinateMap}.
101     *
102     * @param   y0              {@code MIN(y)}
103     * @param   x0              {@code MIN(x)}
104     * @param   yN              {@code MAX(y) + 1}
105     * @param   xN              {@code MAX(x) + 1}
106     *
107     * @return  {@link.this} {@link CoordinateMap}.
108     */
109    public CoordinateMap<V> resize(Number y0, Number x0, Number yN, Number xN) {
110        resize(y0.intValue(), x0.intValue(), yN.intValue(), xN.intValue());
111
112        return this;
113    }
114
115    /**
116     * Method to specify new limits for the {@link CoordinateMap} with
117     * {@code [y0, x0] = [0, 0]}.
118     *
119     * @param   yN              {@code MAX(y) + 1}
120     * @param   xN              {@code MAX(x) + 1}
121     *
122     * @return  {@link.this} {@link CoordinateMap}.
123     */
124    public CoordinateMap<V> resize(Number yN, Number xN) {
125        return resize(0, 0, yN, xN);
126    }
127
128    private void resize(int y0, int x0, int yN, int xN) {
129        min = new Coordinate(Math.min(y0, yN), Math.min(x0, xN));
130        max = new Coordinate(Math.max(y0, yN), Math.max(x0, xN));
131
132        keySet().retainAll(Coordinate.range(min, max));
133
134        fireTableStructureChanged();
135    }
136
137    public Coordinate getMin() { return min; }
138    public Coordinate getMax() { return max; }
139
140    public int getMinY() { return getMin().getY(); }
141    public int getMinX() { return getMin().getX(); }
142
143    public int getMaxY() { return getMax().getY(); }
144    public int getMaxX() { return getMax().getX(); }
145
146    /**
147     * Method to determine if the {@link Coordinate} is included in
148     * {@link.this} {@link CoordinateMap}'s space.  Because the map is
149     * sparse, this method may return {@code true} when
150     * {@link #containsKey(Object)} returns {@code false}.
151     *
152     * @param   coordinate      The {@link Coordinate}.
153     *
154     * @return  {@code true} if within the space; {@code false} otherwise.
155     */
156    public boolean includes(Coordinate coordinate) {
157        return coordinate.within(getMin(), getMax());
158    }
159
160    /**
161     * Method to get a {@link List} of columns
162     * (see {@link #column(Number)}).
163     *
164     * @return  The {@link List} of columns.
165     */
166    public List<CoordinateMap<V>> columns() {
167        ArrayList<CoordinateMap<V>> list = new ArrayList<>(getColumnCount());
168
169        if (getColumnCount() > 0) {
170            for (int x = getMinX(), xN = getMaxX(); x < xN; x += 1) {
171                list.add(column(x));
172            }
173        }
174
175        return list;
176    }
177
178    /**
179     * Method to get a {@link List} of rows (see {@link #row(Number)}).
180     *
181     * @return  The {@link List} of rows.
182     */
183    public List<CoordinateMap<V>> rows() {
184        ArrayList<CoordinateMap<V>> list = new ArrayList<>(getRowCount());
185
186        if (getRowCount() > 0) {
187            for (int y = getMinY(), yN = getMaxY(); y < yN; y += 1) {
188                list.add(row(y));
189            }
190        }
191
192        return list;
193    }
194
195    /**
196     * Method to get the {@link List} representing the specified column
197     * backed by the {@link CoordinateMap}.
198     *
199     * @param   x               The X-coordinate.
200     *
201     * @return  The {@link CoordinateMap} representing the column.
202     */
203    public CoordinateMap<V> column(Number x) {
204        return subMap(getMinY(), x, getMaxY(), x.intValue() + 1);
205    }
206
207    /**
208     * Method to get the {@link List} representing the specified row backed
209     * by the {@link CoordinateMap}.
210     *
211     * @param   y               The Y-coordinate.
212     *
213     * @return  The {@link CoordinateMap} representing the row.
214     */
215    public CoordinateMap<V> row(Number y) {
216        return subMap(y, getMinX(), y.intValue() + 1, getMaxX());
217    }
218
219    /**
220     * Method to get a sub-{@link Map} of {@link.this} {@link Map} also
221     * backed by {@link.this} {@link Map}.
222     *
223     * @param   y0              {@code MIN(y)}
224     * @param   x0              {@code MIN(x)}
225     * @param   yN              {@code MAX(y) + 1}
226     * @param   xN              {@code MAX(x) + 1}
227     *
228     * @return  The sub-{@link Map} ({@link CoordinateMap}).
229     */
230    public CoordinateMap<V> subMap(Number y0, Number x0, Number yN, Number xN) {
231        return new Sub<>(this, y0, x0, yN, xN);
232    }
233
234    /**
235     * Method to get {@link.this} {@link CoordinateMap} values as a
236     * {@link List}.  Updates made through {@link List#set(int,Object)} will
237     * be made to {@link.this} {@link CoordinateMap}.
238     *
239     * @return  The {@link List} of {@link CoordinateMap} values.
240     */
241    public List<V> asList() { return new BackedList(); }
242
243    /**
244     * See {@link #containsKey(Object)}.
245     *
246     * @param   y               The Y-coordinate.
247     * @param   x               The X-coordinate.
248     *
249     * @return  {@code true} if the {@link CoordinateMap} contains a key
250     *          with the specified {@link Coordinate}; {@code false}
251     *          otherwise.
252     */
253    public boolean containsKey(Number y, Number x) {
254        return containsKey(new Coordinate(y, x));
255    }
256
257    /**
258     * See {@link #get(Object)}.
259     *
260     * @param   y               The Y-coordinate.
261     * @param   x               The X-coordinate.
262     *
263     * @return  The value at the coordinate (may be {@code null}).
264     */
265    public V get(Number y, Number x) { return get(new Coordinate(y, x)); }
266
267    /**
268     * See {@link #put(Object,Object)}.
269     *
270     * @param   y               The Y-coordinate.
271     * @param   x               The X-coordinate.
272     * @param   value           The value at the coordinate.
273     *
274     * @return  The previous value at the coordinate.
275     */
276    public V put(Number y, Number x, V value) {
277        return put(new Coordinate(y, x), value);
278    }
279
280    @Override
281    public V put(Coordinate key, V value) {
282        if (min != null) {
283            min = new Coordinate(Math.min(key.getY(), getMinY()), Math.min(key.getX(), getMinX()));
284        } else {
285            min = key;
286        }
287
288        if (max != null) {
289            max = new Coordinate(Math.max(key.getY() + 1, getMaxY()), Math.max(key.getX() + 1, getMaxX()));
290        } else {
291            max = new Coordinate(key.getY() + 1, key.getX() + 1);
292        }
293
294        V old = super.put(key, value);
295
296        fireTableCellUpdated(key.getY() - getMinY(), key.getX() - getMinX());
297
298        return old;
299    }
300
301    @Override
302    public V remove(Object key) {
303        V old = super.remove(key);
304
305        if (key instanceof Coordinate) {
306            Coordinate coordinate = (Coordinate) key;
307
308            fireTableCellUpdated(coordinate.getY() - getMinY(), coordinate.getX() - getMinX());
309        }
310
311        return old;
312    }
313
314    @Override
315    public Comparator<? super Coordinate> comparator() {
316        return map().comparator();
317    }
318
319    @Override
320    public CoordinateMap<V> subMap(Coordinate from, Coordinate to) {
321        throw new UnsupportedOperationException();
322    }
323
324    @Override
325    public CoordinateMap<V> headMap(Coordinate key) {
326        throw new UnsupportedOperationException();
327    }
328
329    @Override
330    public CoordinateMap<V> tailMap(Coordinate key) {
331        throw new UnsupportedOperationException();
332    }
333
334    @Override
335    public Coordinate firstKey() { return map().firstKey(); }
336
337    @Override
338    public Coordinate lastKey() { return map().lastKey(); }
339
340    @Override
341    public void clear() {
342        super.clear();
343        fireTableDataChanged();
344    }
345
346    @Override
347    public int getRowCount() {
348        return (getMax() != null) ? (getMaxY() - getMinY()) : 0;
349    }
350
351    @Override
352    public int getColumnCount() {
353        return (getMax() != null) ? (getMaxX() - getMinX()) : 0;
354    }
355
356    @Override
357    public String getColumnName(int x) { return null; }
358
359    @Override
360    @SuppressWarnings({ "unchecked" })
361    public Class<? extends V> getColumnClass(int x) {
362        return (Class<V>) getType();
363    }
364
365    @Override
366    public boolean isCellEditable(int y, int x) { return false; }
367
368    @Override
369    public V getValueAt(int y, int x) {
370        return get(y - getMinY(), x - getMinX());
371    }
372
373    @Override
374    public void setValueAt(Object value, int y, int x) {
375        put(y - getMinY(), x - getMinX(), getColumnClass(x).cast(value));
376    }
377
378    @Override
379    public void addTableModelListener(TableModelListener listener) {
380        list.add(TableModelListener.class, listener);
381    }
382
383    @Override
384    public void removeTableModelListener(TableModelListener listener) {
385        list.remove(TableModelListener.class, listener);
386    }
387
388    protected TableModelListener[] getTableModelListeners() {
389        return list.getListeners(TableModelListener.class);
390    }
391
392    protected void fireTableDataChanged() {
393        fireTableChanged(new TableModelEvent(this));
394    }
395
396    protected void fireTableStructureChanged() {
397        fireTableChanged(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
398    }
399
400    protected void fireTableRowsInserted(int start, int end) {
401        fireTableChanged(new TableModelEvent(this, start, end, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT));
402    }
403
404    protected void fireTableRowsUpdated(int start, int end) {
405        fireTableChanged(new TableModelEvent(this, start, end, TableModelEvent.ALL_COLUMNS, TableModelEvent.UPDATE));
406    }
407
408    protected void fireTableRowsDeleted(int start, int end) {
409        fireTableChanged(new TableModelEvent(this, start, end, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
410    }
411
412    protected void fireTableCellUpdated(int row, int column) {
413        fireTableChanged(new TableModelEvent(this, row, row, column));
414    }
415
416    protected void fireTableChanged(TableModelEvent event) {
417        TableModelListener[] listeners = getTableModelListeners();
418
419        for (int i = listeners.length - 1; i >= 0; i -= 1) {
420            listeners[i].tableChanged(event);
421        }
422    }
423
424    private static class Sub<V> extends CoordinateMap<V> {
425        private static final long serialVersionUID = -7614329296625073237L;
426
427        public Sub(CoordinateMap<V> map,
428                   Number y0, Number x0, Number yN, Number xN) {
429            super(map, y0, x0, yN, xN);
430        }
431
432        @Override
433        protected CoordinateMap<V> map() {
434            return (CoordinateMap<V>) super.map();
435        }
436
437        @Override
438        public V get(Object key) { return get((Coordinate) key); }
439
440        private V get(Coordinate key) {
441            return key.within(getMin(), getMax()) ? super.get(key) : null;
442        }
443
444        @Override
445        public V put(Coordinate key, V value) {
446            if (! key.within(getMin(), getMax())) {
447                throw new IllegalArgumentException(key + " is outside " + getMin() + " and " + getMax());
448            }
449
450            return super.put(key, value);
451        }
452
453        @Override
454        public V remove(Object key) { return remove((Coordinate) key); }
455
456        private V remove(Coordinate key) {
457            return key.within(getMin(), getMax()) ? super.remove(key) : null;
458        }
459
460        @Override
461        public Set<Entry<Coordinate,V>> entrySet() {
462            entrySet.clear();
463
464            for (Entry<Coordinate,V> entry : map().entrySet()) {
465                if (entry.getKey().within(getMin(), getMax())) {
466                    entrySet.add(entry);
467                }
468            }
469
470            return entrySet;
471        }
472    }
473
474    private class BackedList extends AbstractList<V> {
475        private ArrayList<Coordinate> list = new ArrayList<>();
476
477        public BackedList() {
478            super();
479
480            list.addAll(Coordinate.range(CoordinateMap.this.getMin(), CoordinateMap.this.getMax()));
481        }
482
483        @Override
484        public int size() { return list.size(); }
485
486        @Override
487        public V get(int index) {
488            return CoordinateMap.this.get(list.get(index));
489        }
490
491        @Override
492        public V set(int index, V value) {
493            return CoordinateMap.this.put(list.get(index), value);
494        }
495
496        @Override
497        public void clear() { throw new UnsupportedOperationException(); }
498    }
499}