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}