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